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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
|
# 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
# The band of a CEP that does not exist, as the API returns it on the 404.
#
# 25845000 is the DNE's own start for Areal and does not exist: `cep_ini` is
# the smallest CEP that really is there, one number above it. The 118 CEPs are
# scattered across the ~4,900 numbers up to `cep_fim`.
AREAL_FAIXA = {
"localidade" => "Areal",
"uf" => "RJ",
"estado" => "Rio de Janeiro",
"regiao" => "Sudeste",
"ibge" => "3300225",
"cep_ini" => "25845001",
"cep_fim" => "25849899",
"ceps" => 118
}.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
|