summaryrefslogtreecommitdiffstats
path: root/test/test_helper.rb
blob: e12bd9d1fc61d7ab65129025da7a542d91e80567 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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