Creazione di test per i nuovi endpoint di 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.

Continuiamo la conversazione da

Obiettivi:

  • Per i test: estrarre http://localhost:3000 in una variabile comune

  • Per gli esempi: estrarre host, username e api_key in un file config.yml

Stiamo decidendo dove posizionare e caricare le cose. Attualmente ho nel file discourse_api.rb (con config.yml nella directory radice):

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

… poi in ogni file di esempio:

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

Questo funziona per gli esempi, ma non per i test. Sono interessato a qualche guida sulle best practice per questa PR.

Dividiamo pure le PR. Concentriamoci prima su quella relativa alla specifica e spostiamo la variabile comune nella cartella della specifica. Dovremmo poter aggiungerla al file helper della specifica.

@blake fammi sapere se hai commenti sul design sopra.

Questo va bene.

Poiché questo riguarda solo gli esempi, dovremmo includere tutto ciò che ne è correlato all’interno della directory examples, senza modificare alcuna funzionalità esistente nella directory lib. Anche il file config.yml dovrebbe essere posizionato all’interno della directory examples. Il caricamento del file YAML è molto leggero; possiamo aggiungerlo all’inizio di ogni file di esempio, ma sarebbe meglio inserirlo in un file comune all’interno della directory examples da cui tutti possano leggerlo.

Ecco cosa ho:

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, ad esempio localhost:3000 o discourse.my_domain.com
host: NOME_TUO_HOST

# utente API (può influenzare i risultati restituiti, ad esempio il metodo .categories restituisce solo le categorie visibili al tuo api_username)
# crea un nuovo client quando cambi l'utente API
api_username: LA_TUA_API_KEY

# chiave API dal pannello di amministrazione di Discourse /admin/api/keys
api_key: LA_TUA_API_KEY

examples/badges.rb

require_relative 'example_helper'

# ottieni i badge
puts client.badges

Opinioni? Sicuramente abbiamo bisogno del file yml ora, dato l’example_helper.rb?

Sembra tutto a posto. Aggiorniamo l’esempio di helper con i valori predefiniti in modo che funzioni senza il file yml. Poi aggiorniamo il readme con le istruzioni per copiare il file yml di esempio in examples/config.yml. E assicuriamoci di aggiungere examples/config.yml a .gitignore.

Questa è una convenzione piuttosto standard per evitare che sia facile inviare un file config.yml con le credenziali di produzione. Inoltre, puoi modificare il file senza che git lo rilevi come una modifica.

Tutto a posto, tranne che non ho ben capito questa parte

example_helper.rb funziona così com’è non appena esiste config.yml. Vogliamo che funzioni anche senza un file yml, ma abbiamo ancora un file yml?

Sì, in questo modo Ruby non va in crash se non hanno ancora configurato il file YAML.

Non esattamente così, ma qualcosa del genere:

  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'

Quindi, prima che il file config.yml venga creato, anche con la logica del doppio pipe, si verifica un errore:

`initialize': No such file or directory @ rb_sysopen

Affinché i pipe possano anche solo entrare in gioco, dovremmo modificare anche YAML.load_file.

Sì, dobbiamo modificarlo anche quello.

Quindi questo

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

ci restituisce errori come

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

È questo l’errore preferito?

Penso che quell’errore sia accettabile, poiché è così che funzionano attualmente gli esempi e, comunque, si tratta solo di esempi. Se lo desideri, ora o in seguito, possiamo sempre intercettare l’eccezione e fornire un messaggio utile.

Forse…

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

begin
  CONFIG = YAML.load_file config_yml
rescue Errno::ENOENT
  raise ArgumentError, 'File /examples/config.yml non trovato. Copia config-example.yml per creare un config.yml per il tuo ambiente.'
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

Allora, abbiamo ancora bisogno delle pipe? Le lasciamo?

Sì, funziona. In tal caso non avremo bisogno delle pipe, quindi sì, puoi rimuoverle.

La nostra sezione Testing attuale è un po’ confusa, in quanto mescola attività relative a discourse_api con l’installazione di Discourse stesso. Ad esempio, se hai un server di staging nel cloud, non hai affatto bisogno di installare Discourse in locale. Cosa ne pensi di:

Esempi e Testing

Per provare gli esempi o eseguire i test avrai bisogno di un’istanza di Discourse attiva e funzionante, installata sia in locale che nel cloud.

Esempi

Per eseguire gli esempi in /examples:

  1. Specifica il tuo ambiente creando una copia di config-example.yml per generare un file chiamato config.yml con le impostazioni del tuo ambiente.
  2. In un file di esempio, commenta tutto tranne gli esempi che desideri eseguire.
  3. Modifica il file di esempio con eventuali parametri richiesti, ad esempio username.
  4. L’esecuzione del file utilizzerà quindi il tuo config.yml per eseguire un determinato metodo client (ad esempio client.badges) contro la tua istanza di Discourse.

Testing

Per eseguire i test di discourse_api in spec:

  1. Installa bundler nella directory discourse_api, eseguendo gem install bundler.
  2. All’interno della tua directory discourse_api, esegui: bundle exec rspec spec/.

Suona bene, il readme potrebbe aver bisogno di qualche aggiornamento

Per eseguire i test in realtà non è necessaria un’istanza di Discourse attiva e funzionante. Tutte le richieste di spec dovrebbero essere mockate e non serve un server.

Ah, giusto, quindi che ne dici di …

Esempi

Per provare gli esempi presenti in /examples, avrai bisogno di un’istanza di Discourse in esecuzione, installata sia localmente che nel cloud, poi:

  1. Specifica il tuo ambiente creando una copia di config-example.yml per generare un file chiamato config.yml con le impostazioni del tuo ambiente.
  2. In un file di esempio, commenta tutto tranne gli esempi che desideri eseguire.
  3. Modifica il file di esempio con eventuali parametri richiesti, ad esempio un username di destinazione passato al metodo.
  4. L’esecuzione del file utilizzerà il file config.yml per eseguire un metodo client specifico (ad esempio client.badges) sulla tua istanza di Discourse.

Testing

Per eseguire i test di discourse_api in spec:

  1. Installa bundler nella directory discourse_api, eseguendo gem install bundler
  2. All’interno della directory discourse_api, esegui: bundle exec rspec spec/