diff options
Diffstat (limited to 'test/test_helper.rb')
| -rw-r--r-- | test/test_helper.rb | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..e12bd9d --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +require "json" +require "minitest/autorun" +require "socket" + +require "cepis" + +# A CEP as the API returns it, used as the fixture across the suite. The field +# names stay Portuguese because they mirror the API's JSON. +SE_ADDRESS = { + "cep" => "01001000", + "logradouro" => "Praça da Sé", + "complemento" => "lado ímpar", + "unidade" => "", + "bairro" => "Sé", + "localidade" => "São Paulo", + "uf" => "SP", + "estado" => "São Paulo", + "regiao" => "Sudeste", + "ibge" => "3550308", + "gia" => "1004", + "ddd" => "11", + "siafi" => "7107" +}.freeze + +CSV_HEADER = "cep,logradouro,complemento,unidade,bairro,localidade,uf,estado," \ + "regiao,ibge,gia,ddd,siafi" + +# Per-test programmable routes, plus a request counter. +class Router + attr_reader :calls + attr_accessor :handler + + def initialize + @routes = {} + @calls = [] + @handler = nil + @lock = Mutex.new + end + + # Register the response for a path. + # + # The response may be a `[status, body, headers]` triple or an array of them + # — in which case each request consumes the next one, which is how retrying + # is tested. The last entry repeats once the list runs out. + def respond(path, response) + @lock.synchronize { @routes[path] = response } + end + + # Record the call and return the response registered for `path`. + def next_response(path) + @lock.synchronize do + @calls << path + return @handler.call(path, @calls.length) if @handler + + response = @routes[path] + return [404, JSON.generate({ "erro" => "cep_nao_encontrado" }), {}] if response.nil? + return response unless response.first.is_a?(Array) + + response.length > 1 ? response.shift : response.first + end + end + + # How many requests so far began with `prefix`. + def count(prefix) + @lock.synchronize { @calls.count { |path| path.start_with?(prefix) } } + end +end + +# A throwaway HTTP/1.1 server on a random loopback port. +# +# Deliberately built on TCPServer rather than on a web server gem: the suite +# must run against a stock Ruby, with nothing installed. +class FakeServer + REASONS = { + 200 => "OK", 400 => "Bad Request", 404 => "Not Found", + 429 => "Too Many Requests", 500 => "Internal Server Error", + 502 => "Bad Gateway", 503 => "Service Unavailable" + }.freeze + + attr_reader :router + + def initialize + @router = Router.new + @server = TCPServer.new("127.0.0.1", 0) + @thread = Thread.new { accept_loop } + end + + def url + "http://127.0.0.1:#{@server.addr[1]}" + end + + def stop + @server.close + @thread.join(5) + end + + private + + def accept_loop + loop do + socket = begin + @server.accept + rescue IOError, Errno::EBADF + break + end + Thread.new(socket) { |client| serve(client) } + end + end + + def serve(socket) + request_line = socket.gets + return if request_line.nil? + + path = request_line.split(" ")[1] + while (line = socket.gets) + break if line == "\r\n" + end + + status, body, headers = @router.next_response(path) + write_response(socket, status, body.to_s.b, headers || {}) + rescue Errno::EPIPE, Errno::ECONNRESET, IOError + nil + ensure + begin + socket.close + rescue IOError + nil + end + end + + def write_response(socket, status, body, headers) + head = +"HTTP/1.1 #{status} #{REASONS.fetch(status, 'Unknown')}\r\n" + headers.each { |key, value| head << "#{key}: #{value}\r\n" } + head << "Content-Type: text/plain; charset=utf-8\r\n" + head << "Content-Length: #{body.bytesize}\r\n" + head << "Connection: close\r\n\r\n" + socket.write(head.b + body) + end +end + +# Fixture with a fake server and a client whose backoff is near zero. +# +# The backoff is tiny because what is under test is the retry logic, not +# `sleep`'s ability to sleep. +class ServerTest < Minitest::Test + def setup + @server = FakeServer.new + @client = Cepis::Client.new( + base_url: @server.url, timeout: 5.0, backoff_base: 0.001, backoff_max: 0.01 + ) + end + + def teardown + @server.stop + end + + def routes + @server.router + end + + def csv_body(rows) + "#{CSV_HEADER}\n#{rows.join("\n")}\n" + end +end |