Creando pruebas para nuevos endpoints de discourse_api

Adding some polls API endpoints for PR to discourse_api, which work fine. Now I’m trying to understand how to create tests before submitting the PR, e.g.:

require 'spec_helper'

describe DiscourseApi::API::Polls do
  subject { DiscourseApi::Client.new("http://localhost:3000", "test_d7fd0429940", "test_user" )}

  describe "#polls" do
    before do
      stub_get("http://localhost:3000/polls/voters.json").to_return(body: fixture("voters.json"), headers: { content_type: "application/json" })
    end

    it "requests the correct resource" do
      subject.voters :post_id => 27885, :poll_name => 'poll' 
      expect(a_get("http://localhost:3000/polls/voters.json")).to have_been_made
    end
  end
end

But am getting the error:

   Failed to open TCP connection to localhost:3000 (Connection refused - connect(2) for "localhost" port 3000)
 # ./lib/discourse_api/client.rb:141:in `rescue in request'
 # ./lib/discourse_api/client.rb:132:in `request'
 # ./lib/discourse_api/client.rb:85:in `get'
 # ./lib/discourse_api/api/polls.rb:22:in `voters'
 # ./spec/discourse_api/api/polls_spec.rb:12:in `block (3 levels) in <top (required)>'
 # ------------------
 # --- Caused by: ---
 # Errno::ECONNREFUSED:
 #   Connection refused - connect(2) for "localhost" port 3000
 #   /Users/kimardenmiller/.rbenv/versions/2.5.3/gemsets/d_api/gems/webmock-2.3.2/lib/webmock/http_lib_adapters/net_http.rb:109:in `request'

All the existing tests pass fine for me, and I’m trying to copy the format of those existing tests, but cannot for the life of me figure out what I’m doing wrong.

Here’s the (new) endpoint being tested:

module DiscourseApi
  module API
    module Polls
     def poll_voters(args)
        args = API.params(args)    # post_id, poll_name, user, opts = {}
                  .required(:post_id, :poll_name)
                  .optional(:opts, :api_username)
        response = get("/polls/voters.json", args)
        response[:body]
      end
    end
  end
end

Have working tests, though my gut tells me I could use tips on making those urls more elegant.

require 'spec_helper'

describe DiscourseApi::API::Polls do
  subject { DiscourseApi::Client.new("http://localhost:3000", "test_d7fd0429940", "test_user" )}

  describe "#polls" do
    before do
      stub_get("http://localhost:3000/polls/voters.json?post_id=27885&poll_name=poll").to_return(body: fixture("polls_voters.json"), headers: { content_type: "application/json" })
    end

    it "requests the correct resource" do
      subject.poll_voters post_id: 27885, poll_name: 'poll'
      expect(a_get("http://localhost:3000/polls/voters.json?post_id=27885&poll_name=poll")).to have_been_made
    end

    it "returns the expected votes" do
      voters = subject.poll_voters post_id: 27885, poll_name: 'poll'
      expect(voters).to be_a Hash
      voters.each { |g| expect(g).to be_an Array }
      expect(voters['voters']['e539a9df8700d0d05c69356a07b768cf']).to be_an Array
      expect(voters['voters']['e539a9df8700d0d05c69356a07b768cf'][0]['id']).to eq(356)
    end
    
  end
end

Glad you got the tests working. They can be tricky sometimes because if the request doesn’t match exactly it will try and create an actual request rather that using the stubbed request.

I think the urls are okay. It would be nice not to use such a high post_id, but I’m not concerned about that. And ALL the tests need to have http://localhost:3000 extracted out into a common variable, but that should probably be done in a separate commit. Whenever you are ready you can go ahead and submit a pr and I can review it.

One thing I did notice though is I that passing in the api_username as a parameter is no longer supported.

This is because the discourse_api gem now only passes in the auth via the headers, and discourse core ignores any auth credentials in the body if the header is already being used for auth.

Continuando la conversación desde

Objetivos:

  • Para las pruebas: Extraer http://localhost:3000 en una variable común.

  • Para los ejemplos: Extraer el host, el nombre de usuario y la api_key en un archivo config.yml.

Estamos decidiendo dónde colocar y cargar las cosas. Actualmente tengo en el archivo discourse_api.rb (con config.yml en el directorio raíz):

require 'yaml'
CONFIG = YAML.load_file(File.expand_path('../../config.yml', __FILE__))

… luego en cada archivo de ejemplo:

client = DiscourseApi::Client.new(CONFIG['host'])
client.api_key = CONFIG['api_key']
client.api_username = CONFIG['api_username']

Eso funciona para los ejemplos, pero no para las pruebas. Estoy interesado en algunas orientaciones sobre las mejores prácticas para esta PR.

Por favor, dividamos los PRs. Primero centrémonos en el de la especificación y movamos la variable común a la carpeta de especificaciones. Deberíamos poder añadirla al archivo de ayuda de especificaciones.

@blake, avísame si tienes algún comentario sobre el diseño anterior.

Esto está bien.

Dado que esto es solo para ejemplos, todo lo relacionado con ello debe estar dentro del directorio de ejemplos y no debe modificar ninguna de las funcionalidades existentes en el directorio lib. Incluso el archivo config.yml deberíamos colocarlo dentro del directorio de ejemplos. Cargar el archivo YAML es bastante ligero y podemos añadirlo al principio de cada archivo de ejemplo, pero probablemente sea mejor añadirlo a un archivo común dentro del directorio de ejemplos del que todos puedan leer.

Esto es lo que tengo:

examples/example_helper.rb

$LOAD_PATH.unshift File.expand_path(‘../../lib’, FILE)
require File.expand_path(‘../../lib/discourse_api’, FILE)
require ‘yaml’

CONFIG = YAML.load_file(File.expand_path(‘../../config.yml’, FILE))

def client
discourse_client = DiscourseApi::Client.new(CONFIG[‘host’])
discourse_client.api_key = CONFIG[‘api_key’]
discourse_client.api_username = CONFIG[‘api_username’]
discourse_client
end

examples/config-example.yml

host, por ejemplo localhost:3000 o discourse.mi_dominio.com

host: TU_NOMBRE_DE_HOST

usuario de la API (puede afectar los resultados devueltos, por ejemplo, el método .categories devuelve solo las categorías que tu api_username puede ver)

crea un nuevo cliente al cambiar el usuario de la API

api_username: TU_USUARIO_API

clave de la API desde el panel de administración de Discourse /admin/api/keys

api_key: TU_CLAVE_API

examples/badges.rb

require_relative ‘example_helper’

obtener insignias

puts client.badges

¿Qué opinas? ¿Realmente necesitamos el archivo YML ahora, dado que está example_helper.rb?

Se ve bien. Actualicemos el helper de ejemplo con valores predeterminados para que funcione sin el archivo yml. Luego, actualicemos el readme con instrucciones para copiar el archivo yml de ejemplo a examples/config.yml. Y asegúrate de agregar examples/config.yml al archivo .gitignore.

Esta es una convención bastante estándar para que no sea fácil enviar un archivo config.yml con credenciales de producción. Además, puedes editar el archivo sin que git lo detecte como un cambio.

Todo bien, excepto que no termino de entender este punto

example_helper.rb funciona tal cual una vez que existe config.yml. Queremos que también funcione sin un archivo yml, pero ¿aún tenemos un archivo yml?

Sí, de esta manera Ruby no explota si aún no han configurado su archivo YAML.

No exactamente esto, pero algo como:

  discourse_client = DiscourseApi::Client.new(CONFIG['host'] || 'localhost:3000')
  discourse_client.api_key = CONFIG['api_key'] || 'api-key'
  discourse_client.api_username = CONFIG['api_username'] || 'api-username'

Así que, antes de que se cree el archivo config.yml, incluso con la lógica del doble pipe, tenemos un error:

`initialize': No such file or directory @ rb_sysopen

Para que los pipes siquiera puedan entrar en juego, también tendríamos que ajustar YAML.load_file.

Sí, también tenemos que ajustar eso.

Así que esto

config_yml = File.expand_path('../config.yml', __FILE__)

if File.exists? config_yml
  CONFIG = YAML.load_file config_yml
else
  CONFIG = {}
end

def client
  discourse_client = DiscourseApi::Client.new(CONFIG['host'] || 'http://localhost:3000')
  discourse_client.api_key = CONFIG['api_key'] || 'YOUR_API_KEY'
  discourse_client.api_username = CONFIG['api_username'] || 'YOUR_USERNAME'
  discourse_client
end

nos da errores como

Failed to open TCP connection to localhost:3000 (Connection refused - connect(2) for "localhost" port 3000) (Faraday::ConnectionFailed)

¿Es ese nuestro error preferido?

Creo que ese error está bien, ya que así es como funcionan actualmente los ejemplos, y además esto solo está en los ejemplos. Si lo deseas, ahora o más tarde, siempre podemos capturar la excepción y proporcionar un mensaje útil.

Tal vez …

config_yml = File.expand_path('../configg.yml', __FILE__)

begin
  CONFIG = YAML.load_file config_yml
rescue Errno::ENOENT
  raise ArgumentError, '/examples/config.yml file not found. Please copy config-example.yml to create a config.yml for your environment.'
end

def client
  discourse_client = DiscourseApi::Client.new(CONFIG['host'] || 'http://localhost:3000')
  discourse_client.api_key = CONFIG['api_key'] || 'YOUR_API_KEY'
  discourse_client.api_username = CONFIG['api_username'] || 'YOUR_USERNAME'
  discourse_client
end

¿Seguimos necesitando los pipes entonces? ¿Los dejamos?

Sí, eso funciona. En ese caso, no necesitaremos las tuberías, así que sí, puedes eliminarlas.

Nuestra sección de Pruebas actual me resulta un poco confusa, ya que mezcla tareas relacionadas con discourse_api con la instalación del propio Discourse. Por ejemplo, si tienes un servidor de staging en la nube, no necesitas instalar Discourse localmente en absoluto. ¿Qué opinas de lo siguiente:

Ejemplos y Pruebas

Para probar los ejemplos o ejecutar las pruebas, necesitarás una instancia de Discourse en funcionamiento, instalada ya sea localmente o en la nube.

Ejemplos

Para ejecutar los ejemplos en /examples:

  1. Especifica tu entorno copiando el archivo config-example.yml para crear un archivo llamado config.yml con la configuración de tu entorno.
  2. En un archivo de ejemplo dado, comenta todos los ejemplos excepto los que quieras ejecutar.
  3. Edita el archivo de ejemplo con los parámetros requeridos, por ejemplo username.
  4. Al ejecutar el archivo, se leerá de tu config.yml para ejecutar un método de cliente dado (por ejemplo, client.badges) contra tu instancia de Discourse.

Pruebas

Para ejecutar las pruebas de discourse_api en spec:

  1. Instala bundler en el directorio discourse_api ejecutando gem install bundler.
  2. Dentro de tu directorio discourse_api, ejecuta: bundle exec rspec spec/.

Suena bien, el README podría necesitar algunas actualizaciones

Para ejecutar las pruebas, en realidad no necesitas una instancia de Discourse funcionando. Todas las solicitudes de las especificaciones deben estar simuladas y no se requiere un servidor.

Ah, claro, entonces ¿qué tal si…

Ejemplos

Para probar los ejemplos en /examples, necesitarás tener una instancia de Discourse funcionando, instalada ya sea localmente o en la nube, y luego:

  1. Especifica tu entorno copiando el archivo config-example.yml para crear un archivo llamado config.yml con la configuración de tu entorno.
  2. En un archivo de ejemplo dado, comenta todo excepto los ejemplos que deseas ejecutar.
  3. Edita el archivo de ejemplo con los parámetros necesarios, por ejemplo, un username de destino pasado al método.
  4. Al ejecutar el archivo, se leerá desde tu config.yml para ejecutar un método de cliente dado (por ejemplo, client.badges) contra tu instancia de Discourse.

Pruebas

Para ejecutar las pruebas de discourse_api en spec:

  1. Instala bundler en el directorio de discourse_api ejecutando gem install bundler.
  2. Dentro del directorio de discourse_api, ejecuta: bundle exec rspec spec/.