# 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