focus_nfe 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. checksums.yaml +7 -0
  2. data/.git-hooks/pre_push/steep.rb +18 -0
  3. data/.git-hooks/pre_push/yard_doc.rb +18 -0
  4. data/.gitattributes +1 -0
  5. data/.overcommit.yml +69 -0
  6. data/.rspec +3 -0
  7. data/.yardopts +11 -0
  8. data/CHANGELOG.md +77 -0
  9. data/CLAUDE.md +118 -0
  10. data/CODE_OF_CONDUCT.md +10 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +348 -0
  13. data/Rakefile +105 -0
  14. data/data/schemas/schema_cte.json +2793 -0
  15. data/data/schemas/schema_cte_os.json +1335 -0
  16. data/data/schemas/schema_cte_os_transporte_rodoviario.json +109 -0
  17. data/data/schemas/schema_cte_transporte_aereo.json +115 -0
  18. data/data/schemas/schema_cte_transporte_aquaviario.json +174 -0
  19. data/data/schemas/schema_cte_transporte_dutoviario.json +65 -0
  20. data/data/schemas/schema_cte_transporte_ferroviario.json +144 -0
  21. data/data/schemas/schema_cte_transporte_multimodal.json +45 -0
  22. data/data/schemas/schema_cte_transporte_rodoviario.json +78 -0
  23. data/data/schemas/schema_dce.json +549 -0
  24. data/data/schemas/schema_mdfe.json +1102 -0
  25. data/data/schemas/schema_mdfe_transporte_aereo.json +44 -0
  26. data/data/schemas/schema_mdfe_transporte_aquaviario.json +209 -0
  27. data/data/schemas/schema_mdfe_transporte_ferroviario.json +99 -0
  28. data/data/schemas/schema_mdfe_transporte_rodoviario.json +628 -0
  29. data/data/schemas/schema_nfcom.json +1859 -0
  30. data/data/schemas/schema_nfe.json +4750 -0
  31. data/data/schemas/schema_nfe_forma_pagamento.json +97 -0
  32. data/data/schemas/schema_nfe_item.json +2574 -0
  33. data/data/schemas/schema_nfgas.json +2316 -0
  34. data/data/schemas/schema_nfse_nacional.json +1847 -0
  35. data/data/schemas/schema_nfse_recebida.json +548 -0
  36. data/lib/focus_nfe/client.rb +162 -0
  37. data/lib/focus_nfe/configuration.rb +104 -0
  38. data/lib/focus_nfe/errors.rb +123 -0
  39. data/lib/focus_nfe/esquemas/campo.rb +171 -0
  40. data/lib/focus_nfe/esquemas/catalogo.rb +34 -0
  41. data/lib/focus_nfe/esquemas/decimal.rb +66 -0
  42. data/lib/focus_nfe/esquemas/esquema.rb +72 -0
  43. data/lib/focus_nfe/esquemas/validador.rb +87 -0
  44. data/lib/focus_nfe/http/adapter.rb +25 -0
  45. data/lib/focus_nfe/http/adapters/net_http.rb +99 -0
  46. data/lib/focus_nfe/http/authentication.rb +23 -0
  47. data/lib/focus_nfe/http/connection.rb +118 -0
  48. data/lib/focus_nfe/http/logging.rb +100 -0
  49. data/lib/focus_nfe/http/response.rb +75 -0
  50. data/lib/focus_nfe/modelos/documento.rb +113 -0
  51. data/lib/focus_nfe/modelos/inutilizacao.rb +75 -0
  52. data/lib/focus_nfe/modelos/pagina.rb +54 -0
  53. data/lib/focus_nfe/recursos/backups.rb +17 -0
  54. data/lib/focus_nfe/recursos/base.rb +91 -0
  55. data/lib/focus_nfe/recursos/ceps.rb +16 -0
  56. data/lib/focus_nfe/recursos/cfops.rb +16 -0
  57. data/lib/focus_nfe/recursos/cnaes.rb +16 -0
  58. data/lib/focus_nfe/recursos/cnpjs.rb +13 -0
  59. data/lib/focus_nfe/recursos/concerns/baixavel.rb +41 -0
  60. data/lib/focus_nfe/recursos/concerns/baixavel_eventos.rb +34 -0
  61. data/lib/focus_nfe/recursos/concerns/cancelavel.rb +25 -0
  62. data/lib/focus_nfe/recursos/concerns/conciliavel.rb +66 -0
  63. data/lib/focus_nfe/recursos/concerns/consultavel.rb +26 -0
  64. data/lib/focus_nfe/recursos/concerns/corrigivel.rb +45 -0
  65. data/lib/focus_nfe/recursos/concerns/corrigivel_cte.rb +60 -0
  66. data/lib/focus_nfe/recursos/concerns/emitivel.rb +51 -0
  67. data/lib/focus_nfe/recursos/concerns/enviavel.rb +40 -0
  68. data/lib/focus_nfe/recursos/concerns/eventavel.rb +46 -0
  69. data/lib/focus_nfe/recursos/concerns/inutilizavel.rb +96 -0
  70. data/lib/focus_nfe/recursos/concerns/listavel.rb +22 -0
  71. data/lib/focus_nfe/recursos/concerns/localizavel.rb +22 -0
  72. data/lib/focus_nfe/recursos/concerns/notificavel.rb +23 -0
  73. data/lib/focus_nfe/recursos/concerns/removivel.rb +20 -0
  74. data/lib/focus_nfe/recursos/concerns/visualizavel.rb +28 -0
  75. data/lib/focus_nfe/recursos/cte.rb +35 -0
  76. data/lib/focus_nfe/recursos/cte_os.rb +29 -0
  77. data/lib/focus_nfe/recursos/ctes_recebidas.rb +38 -0
  78. data/lib/focus_nfe/recursos/dce.rb +16 -0
  79. data/lib/focus_nfe/recursos/emails_bloqueados.rb +31 -0
  80. data/lib/focus_nfe/recursos/empresas.rb +35 -0
  81. data/lib/focus_nfe/recursos/mdfe.rb +78 -0
  82. data/lib/focus_nfe/recursos/municipios.rb +60 -0
  83. data/lib/focus_nfe/recursos/ncms.rb +16 -0
  84. data/lib/focus_nfe/recursos/nfce.rb +19 -0
  85. data/lib/focus_nfe/recursos/nfcom.rb +16 -0
  86. data/lib/focus_nfe/recursos/nfe.rb +106 -0
  87. data/lib/focus_nfe/recursos/nfes_recebidas.rb +56 -0
  88. data/lib/focus_nfe/recursos/nfgas.rb +16 -0
  89. data/lib/focus_nfe/recursos/nfse.rb +18 -0
  90. data/lib/focus_nfe/recursos/nfse_nacional.rb +16 -0
  91. data/lib/focus_nfe/recursos/nfses_nacionais_recebidas.rb +21 -0
  92. data/lib/focus_nfe/recursos/webhooks.rb +22 -0
  93. data/lib/focus_nfe/version.rb +6 -0
  94. data/lib/focus_nfe/webhook.rb +68 -0
  95. data/lib/focus_nfe.rb +124 -0
  96. data/sig/focus_nfe/client.rbs +38 -0
  97. data/sig/focus_nfe/configuration.rbs +29 -0
  98. data/sig/focus_nfe/errors.rbs +59 -0
  99. data/sig/focus_nfe/esquemas/campo.rbs +47 -0
  100. data/sig/focus_nfe/esquemas/catalogo.rbs +8 -0
  101. data/sig/focus_nfe/esquemas/decimal.rbs +25 -0
  102. data/sig/focus_nfe/esquemas/esquema.rbs +30 -0
  103. data/sig/focus_nfe/esquemas/validador.rbs +20 -0
  104. data/sig/focus_nfe/http/adapter.rbs +7 -0
  105. data/sig/focus_nfe/http/adapters/net_http.rbs +25 -0
  106. data/sig/focus_nfe/http/authentication.rbs +9 -0
  107. data/sig/focus_nfe/http/connection.rbs +32 -0
  108. data/sig/focus_nfe/http/logging.rbs +30 -0
  109. data/sig/focus_nfe/http/response.rbs +28 -0
  110. data/sig/focus_nfe/modelos/documento.rbs +34 -0
  111. data/sig/focus_nfe/modelos/inutilizacao.rbs +24 -0
  112. data/sig/focus_nfe/modelos/pagina.rbs +21 -0
  113. data/sig/focus_nfe/recursos/backups.rbs +7 -0
  114. data/sig/focus_nfe/recursos/base.rbs +25 -0
  115. data/sig/focus_nfe/recursos/ceps.rbs +10 -0
  116. data/sig/focus_nfe/recursos/cfops.rbs +10 -0
  117. data/sig/focus_nfe/recursos/cnaes.rbs +10 -0
  118. data/sig/focus_nfe/recursos/cnpjs.rbs +7 -0
  119. data/sig/focus_nfe/recursos/concerns/baixavel.rbs +12 -0
  120. data/sig/focus_nfe/recursos/concerns/baixavel_eventos.rbs +14 -0
  121. data/sig/focus_nfe/recursos/concerns/cancelavel.rbs +9 -0
  122. data/sig/focus_nfe/recursos/concerns/conciliavel.rbs +15 -0
  123. data/sig/focus_nfe/recursos/concerns/consultavel.rbs +9 -0
  124. data/sig/focus_nfe/recursos/concerns/corrigivel.rbs +15 -0
  125. data/sig/focus_nfe/recursos/concerns/corrigivel_cte.rbs +17 -0
  126. data/sig/focus_nfe/recursos/concerns/emitivel.rbs +14 -0
  127. data/sig/focus_nfe/recursos/concerns/enviavel.rbs +15 -0
  128. data/sig/focus_nfe/recursos/concerns/eventavel.rbs +12 -0
  129. data/sig/focus_nfe/recursos/concerns/inutilizavel.rbs +20 -0
  130. data/sig/focus_nfe/recursos/concerns/listavel.rbs +9 -0
  131. data/sig/focus_nfe/recursos/concerns/localizavel.rbs +9 -0
  132. data/sig/focus_nfe/recursos/concerns/notificavel.rbs +9 -0
  133. data/sig/focus_nfe/recursos/concerns/removivel.rbs +9 -0
  134. data/sig/focus_nfe/recursos/concerns/visualizavel.rbs +9 -0
  135. data/sig/focus_nfe/recursos/cte.rbs +17 -0
  136. data/sig/focus_nfe/recursos/cte_os.rbs +17 -0
  137. data/sig/focus_nfe/recursos/ctes_recebidas.rbs +14 -0
  138. data/sig/focus_nfe/recursos/dce.rbs +10 -0
  139. data/sig/focus_nfe/recursos/emails_bloqueados.rbs +12 -0
  140. data/sig/focus_nfe/recursos/empresas.rbs +12 -0
  141. data/sig/focus_nfe/recursos/mdfe.rbs +21 -0
  142. data/sig/focus_nfe/recursos/municipios.rbs +19 -0
  143. data/sig/focus_nfe/recursos/ncms.rbs +10 -0
  144. data/sig/focus_nfe/recursos/nfce.rbs +12 -0
  145. data/sig/focus_nfe/recursos/nfcom.rbs +10 -0
  146. data/sig/focus_nfe/recursos/nfe.rbs +23 -0
  147. data/sig/focus_nfe/recursos/nfes_recebidas.rbs +16 -0
  148. data/sig/focus_nfe/recursos/nfgas.rbs +10 -0
  149. data/sig/focus_nfe/recursos/nfse.rbs +11 -0
  150. data/sig/focus_nfe/recursos/nfse_nacional.rbs +10 -0
  151. data/sig/focus_nfe/recursos/nfses_nacionais_recebidas.rbs +11 -0
  152. data/sig/focus_nfe/recursos/webhooks.rbs +11 -0
  153. data/sig/focus_nfe/webhook.rbs +11 -0
  154. data/sig/focus_nfe.rbs +10 -0
  155. data/spec/focus_nfe/client_spec.rb +208 -0
  156. data/spec/focus_nfe/configuration_spec.rb +121 -0
  157. data/spec/focus_nfe/errors_mapping_spec.rb +68 -0
  158. data/spec/focus_nfe/errors_spec.rb +107 -0
  159. data/spec/focus_nfe/esquemas/campo_spec.rb +291 -0
  160. data/spec/focus_nfe/esquemas/decimal_spec.rb +54 -0
  161. data/spec/focus_nfe/esquemas/esquema_spec.rb +73 -0
  162. data/spec/focus_nfe/esquemas/validador_spec.rb +167 -0
  163. data/spec/focus_nfe/esquemas_spec.rb +42 -0
  164. data/spec/focus_nfe/http/adapter_spec.rb +8 -0
  165. data/spec/focus_nfe/http/adapters/net_http_spec.rb +181 -0
  166. data/spec/focus_nfe/http/authentication_spec.rb +24 -0
  167. data/spec/focus_nfe/http/connection_spec.rb +255 -0
  168. data/spec/focus_nfe/http/logging_spec.rb +83 -0
  169. data/spec/focus_nfe/http/response_spec.rb +161 -0
  170. data/spec/focus_nfe/modelos/documento_spec.rb +150 -0
  171. data/spec/focus_nfe/modelos/inutilizacao_spec.rb +91 -0
  172. data/spec/focus_nfe/modelos/pagina_spec.rb +77 -0
  173. data/spec/focus_nfe/recursos/backups_spec.rb +29 -0
  174. data/spec/focus_nfe/recursos/base_spec.rb +56 -0
  175. data/spec/focus_nfe/recursos/ceps_spec.rb +16 -0
  176. data/spec/focus_nfe/recursos/cfops_spec.rb +16 -0
  177. data/spec/focus_nfe/recursos/cnaes_spec.rb +20 -0
  178. data/spec/focus_nfe/recursos/cnpjs_spec.rb +11 -0
  179. data/spec/focus_nfe/recursos/concerns/emitivel_validacao_spec.rb +158 -0
  180. data/spec/focus_nfe/recursos/cte_os_spec.rb +9 -0
  181. data/spec/focus_nfe/recursos/cte_spec.rb +9 -0
  182. data/spec/focus_nfe/recursos/ctes_recebidas_spec.rb +56 -0
  183. data/spec/focus_nfe/recursos/dce_spec.rb +8 -0
  184. data/spec/focus_nfe/recursos/emails_bloqueados_spec.rb +29 -0
  185. data/spec/focus_nfe/recursos/empresas_spec.rb +45 -0
  186. data/spec/focus_nfe/recursos/mdfe_spec.rb +100 -0
  187. data/spec/focus_nfe/recursos/municipios_spec.rb +58 -0
  188. data/spec/focus_nfe/recursos/ncms_spec.rb +16 -0
  189. data/spec/focus_nfe/recursos/nfce_spec.rb +10 -0
  190. data/spec/focus_nfe/recursos/nfcom_spec.rb +8 -0
  191. data/spec/focus_nfe/recursos/nfe_spec.rb +262 -0
  192. data/spec/focus_nfe/recursos/nfes_recebidas_spec.rb +87 -0
  193. data/spec/focus_nfe/recursos/nfgas_spec.rb +8 -0
  194. data/spec/focus_nfe/recursos/nfse_nacional_spec.rb +8 -0
  195. data/spec/focus_nfe/recursos/nfse_spec.rb +9 -0
  196. data/spec/focus_nfe/recursos/nfses_nacionais_recebidas_spec.rb +17 -0
  197. data/spec/focus_nfe/recursos/webhooks_spec.rb +22 -0
  198. data/spec/focus_nfe/webhook_spec.rb +66 -0
  199. data/spec/focus_nfe_global_configuration_spec.rb +70 -0
  200. data/spec/focus_nfe_require_spec.rb +87 -0
  201. data/spec/focus_nfe_spec.rb +11 -0
  202. data/spec/spec_helper.rb +58 -0
  203. data/spec/support/shared_examples/recurso_fiscal.rb +445 -0
  204. data/spec/support/shared_examples/recurso_leitura.rb +217 -0
  205. data/tools/pull_fields.rb +62 -0
  206. metadata +420 -0
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "stringio"
5
+
6
+ RSpec.describe FocusNfe::HTTP::Connection do
7
+ subject(:connection) { described_class.new(config, token: "tok") }
8
+
9
+ let(:config) { FocusNfe::Configuration.new(token_empresa: "tok", environment: environment, headers: extras) }
10
+ let(:environment) { :homologacao }
11
+ let(:extras) { {} }
12
+
13
+ def homologacao = "https://homologacao.focusnfe.com.br"
14
+ def producao = "https://api.focusnfe.com.br"
15
+
16
+ def authorization(token)
17
+ FocusNfe::HTTP::Authentication.header(token).fetch("Authorization")
18
+ end
19
+
20
+ def sent_header(url, headers)
21
+ a_request(:get, url).with(headers: headers)
22
+ end
23
+
24
+ describe "montagem de URL" do
25
+ it "monta base_url + /v2/ + caminho em homologação" do
26
+ stub = stub_request(:get, "#{homologacao}/v2/nfe").to_return(status: 200, body: "")
27
+
28
+ connection.get("nfe")
29
+
30
+ expect(stub).to have_been_requested
31
+ end
32
+
33
+ context "quando ambiente é produção" do
34
+ let(:environment) { :producao }
35
+
36
+ it "usa o host de produção" do
37
+ stub = stub_request(:get, "#{producao}/v2/nfe").to_return(status: 200, body: "")
38
+
39
+ connection.get("nfe")
40
+
41
+ expect(stub).to have_been_requested
42
+ end
43
+ end
44
+
45
+ it "normaliza barra inicial no caminho" do
46
+ stub = stub_request(:get, "#{homologacao}/v2/nfe").to_return(status: 200, body: "")
47
+
48
+ connection.get("/nfe")
49
+
50
+ expect(stub).to have_been_requested
51
+ end
52
+
53
+ it "codifica params como query string" do
54
+ stub = stub_request(:get, "#{homologacao}/v2/nfe").with(query: { ref: "pedido-42" })
55
+ stub.to_return(status: 200, body: "")
56
+
57
+ connection.get("nfe", params: { ref: "pedido-42" })
58
+
59
+ expect(stub).to have_been_requested
60
+ end
61
+ end
62
+
63
+ describe "verbos" do
64
+ it "expõe get, post, put e delete", :aggregate_failures do
65
+ %i[get post put delete].each { |verb| expect(connection).to respond_to(verb) }
66
+ end
67
+
68
+ it "serializa corpo Hash para JSON no POST" do
69
+ stub = stub_request(:post, "#{homologacao}/v2/nfe").with(body: '{"ref":"x"}').to_return(status: 200, body: "")
70
+
71
+ connection.post("nfe", body: { ref: "x" })
72
+
73
+ expect(stub).to have_been_requested
74
+ end
75
+
76
+ it "não envia corpo quando body: é nil" do
77
+ stub = stub_request(:get, "#{homologacao}/v2/nfe").with { |req| req.body.nil? || req.body.empty? }
78
+ stub.to_return(status: 200, body: "")
79
+
80
+ connection.get("nfe")
81
+
82
+ expect(stub).to have_been_requested
83
+ end
84
+
85
+ it "envia corpo no DELETE (cancelamento com justificativa)" do
86
+ stub = stub_request(:delete, "#{homologacao}/v2/nfe/42").with(body: '{"justificativa":"erro"}')
87
+ stub.to_return(status: 200, body: "")
88
+
89
+ connection.delete("nfe/42", body: { justificativa: "erro" })
90
+
91
+ expect(stub).to have_been_requested
92
+ end
93
+ end
94
+
95
+ describe "cabeçalhos padrão" do
96
+ let(:url) { "#{homologacao}/v2/nfe" }
97
+
98
+ before { stub_request(:get, url).to_return(status: 200, body: "") }
99
+
100
+ it "envia Content-Type e Accept JSON" do
101
+ connection.get("nfe")
102
+
103
+ headers = { "Content-Type" => "application/json", "Accept" => "application/json" }
104
+ expect(sent_header(url, headers)).to have_been_made
105
+ end
106
+
107
+ it "envia User-Agent baseado em FocusNfe::VERSION" do
108
+ connection.get("nfe")
109
+
110
+ expect(sent_header(url, "User-Agent" => "focus_nfe/#{FocusNfe::VERSION}")).to have_been_made
111
+ end
112
+
113
+ it "envia o Authorization Basic do token" do
114
+ connection.get("nfe")
115
+
116
+ expect(sent_header(url, "Authorization" => authorization("tok"))).to have_been_made
117
+ end
118
+ end
119
+
120
+ describe "precedência de cabeçalhos" do
121
+ let(:url) { "#{homologacao}/v2/nfe" }
122
+
123
+ before do
124
+ stub_request(:get, url).to_return(status: 200, body: "")
125
+ stub_request(:post, url).to_return(status: 200, body: "")
126
+ end
127
+
128
+ context "com extra customizado na config" do
129
+ let(:extras) { { "X-Empresa" => "loja-1" } }
130
+
131
+ it "adiciona o header extra à requisição" do
132
+ connection.get("nfe")
133
+
134
+ expect(sent_header(url, "X-Empresa" => "loja-1")).to have_been_made
135
+ end
136
+ end
137
+
138
+ context "com extra tentando trocar o Authorization" do
139
+ let(:extras) { { "Authorization" => "Basic invasor" } }
140
+
141
+ it "mantém o Authorization calculado" do
142
+ connection.get("nfe")
143
+
144
+ expect(sent_header(url, "Authorization" => authorization("tok"))).to have_been_made
145
+ end
146
+ end
147
+
148
+ it "permite a chamada sobrescrever o Content-Type (ex.: XML)" do
149
+ connection.post("nfe", body: "<x/>", headers: { "Content-Type" => "application/xml" })
150
+
151
+ expect(a_request(:post, url).with(headers: { "Content-Type" => "application/xml" })).to have_been_made
152
+ end
153
+
154
+ it "ignora um Authorization per-call, mantendo o calculado" do
155
+ connection.get("nfe", headers: { "Authorization" => "Basic invasor" })
156
+
157
+ expect(sent_header(url, "Authorization" => authorization("tok"))).to have_been_made
158
+ end
159
+ end
160
+
161
+ describe "respostas" do
162
+ let(:url) { "#{homologacao}/v2/nfe" }
163
+ let(:json) { { "Content-Type" => "application/json" } }
164
+
165
+ it "devolve a Response em 2xx", :aggregate_failures do
166
+ stub_request(:get, url).to_return(status: 200, body: '{"ok":true}', headers: json)
167
+
168
+ response = connection.get("nfe")
169
+
170
+ expect(response).to be_a(FocusNfe::HTTP::Response)
171
+ expect(response.body).to eq("ok" => true)
172
+ end
173
+
174
+ {
175
+ 400 => FocusNfe::Errors::BadRequest,
176
+ 401 => FocusNfe::Errors::Unauthorized,
177
+ 403 => FocusNfe::Errors::Forbidden,
178
+ 404 => FocusNfe::Errors::NotFound,
179
+ 409 => FocusNfe::Errors::Conflict,
180
+ 422 => FocusNfe::Errors::ValidationError,
181
+ 429 => FocusNfe::Errors::RateLimited,
182
+ 500 => FocusNfe::Errors::ServerError,
183
+ 418 => FocusNfe::Errors::UnexpectedResponse
184
+ }.each do |status, klass|
185
+ it "levanta #{klass} em status #{status}" do
186
+ stub_request(:get, url).to_return(status: status, body: "")
187
+
188
+ expect { connection.get("nfe") }.to raise_error(klass)
189
+ end
190
+ end
191
+
192
+ it "preenche a exceção com status e corpo da resposta", :aggregate_failures do
193
+ stub_request(:get, url).to_return(status: 422, body: '{"erro":"ref"}', headers: json)
194
+
195
+ expect { connection.get("nfe") }.to raise_error do |error|
196
+ expect(error).to have_attributes(status: 422, body: { "erro" => "ref" })
197
+ end
198
+ end
199
+ end
200
+
201
+ describe "logging" do
202
+ let(:url) { "#{homologacao}/v2/nfe" }
203
+ let(:json) { { "Content-Type" => "application/json" } }
204
+ let(:io) { StringIO.new }
205
+ let(:logger) { Logger.new(io).tap { |l| l.level = Logger::DEBUG } }
206
+ let(:config) do
207
+ FocusNfe::Configuration.new(token_empresa: "tok", environment: environment, logger: logger)
208
+ end
209
+
210
+ it "registra requisição em debug e resposta 2xx em info", :aggregate_failures do
211
+ stub_request(:get, url).to_return(status: 200, body: "")
212
+
213
+ connection.get("nfe")
214
+
215
+ expect(io.string).to match(/DEBUG.*→ GET/)
216
+ expect(io.string).to match(/INFO.*← 200 GET/)
217
+ end
218
+
219
+ it "redige o Authorization e não vaza o token", :aggregate_failures do
220
+ stub_request(:get, url).to_return(status: 200, body: "")
221
+
222
+ connection.get("nfe")
223
+
224
+ expect(io.string).to include("[FILTERED]")
225
+ expect(io.string).not_to include(authorization("tok"))
226
+ end
227
+
228
+ it "registra resposta não-2xx em warn com o corpo de erro", :aggregate_failures do
229
+ stub_request(:get, url).to_return(status: 422, body: '{"erro":"ref"}', headers: json)
230
+
231
+ expect { connection.get("nfe") }.to raise_error(FocusNfe::Errors::ValidationError)
232
+ expect(io.string).to match(/WARN.*← 422 GET/)
233
+ expect(io.string).to include("ref")
234
+ end
235
+
236
+ it "registra falha de transporte em error", :aggregate_failures do
237
+ stub_request(:get, url).to_timeout
238
+
239
+ expect { connection.get("nfe") }.to raise_error(FocusNfe::Errors::ConnectionError)
240
+ expect(io.string).to match(/ERROR.*✕ GET/)
241
+ end
242
+
243
+ context "sem logger configurado (padrão nil)" do
244
+ let(:config) { FocusNfe::Configuration.new(token_empresa: "tok", environment: environment) }
245
+
246
+ it "não quebra o fluxo de requisição" do
247
+ stub = stub_request(:get, url).to_return(status: 200, body: "")
248
+
249
+ connection.get("nfe")
250
+
251
+ expect(stub).to have_been_requested
252
+ end
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "stringio"
5
+
6
+ RSpec.describe FocusNfe::HTTP::Logging do
7
+ subject(:logging) { described_class.new(logger) }
8
+
9
+ let(:io) { StringIO.new }
10
+ let(:logger) { Logger.new(io).tap { |l| l.level = Logger::DEBUG } }
11
+ let(:output) { io.string }
12
+
13
+ let(:authorization) { FocusNfe::HTTP::Authentication.header("segredo").fetch("Authorization") }
14
+ let(:headers) { { "Content-Type" => "application/json", "Authorization" => authorization } }
15
+
16
+ describe "logger nil (padrão)" do
17
+ let(:logger) { nil }
18
+
19
+ it "é um no-op em todos os métodos", :aggregate_failures do
20
+ expect { logging.request(:get, "https://x/v2/nfe", headers) }.not_to raise_error
21
+ expect { logging.response(:get, "https://x/v2/nfe", 200, 0.1, nil) }.not_to raise_error
22
+ expect { logging.response(:get, "https://x/v2/nfe", 422, 0.1, '{"erro":"x"}') }.not_to raise_error
23
+ expect { logging.failure(:get, "https://x/v2/nfe", StandardError.new("boom"), 0.1) }.not_to raise_error
24
+ end
25
+ end
26
+
27
+ describe "#request" do
28
+ before { logging.request(:post, "https://x/v2/nfe", headers) }
29
+
30
+ it "registra em nível DEBUG com verbo e URL", :aggregate_failures do
31
+ expect(output).to match(/DEBUG/)
32
+ expect(output).to include("POST")
33
+ expect(output).to include("https://x/v2/nfe")
34
+ end
35
+
36
+ it "redige o Authorization e não vaza o valor real", :aggregate_failures do
37
+ expect(output).to include("[FILTERED]")
38
+ expect(output).not_to include(authorization)
39
+ expect(output).not_to include("Basic")
40
+ end
41
+
42
+ it "preserva headers não sensíveis" do
43
+ expect(output).to include("Content-Type")
44
+ end
45
+ end
46
+
47
+ describe "#response" do
48
+ it "registra 2xx em nível INFO com status e tempo, sem corpo", :aggregate_failures do
49
+ logging.response(:post, "https://x/v2/nfe", 200, 0.123, '{"status":"ok"}')
50
+
51
+ expect(output).to match(/INFO/)
52
+ expect(output).to include("200")
53
+ expect(output).to include("123ms")
54
+ expect(output).not_to include('"status":"ok"')
55
+ end
56
+
57
+ it "registra não-2xx em nível WARN incluindo o corpo de erro", :aggregate_failures do
58
+ logging.response(:post, "https://x/v2/nfe", 422, 0.05, '{"erro":"ref invalida"}')
59
+
60
+ expect(output).to match(/WARN/)
61
+ expect(output).to include("422")
62
+ expect(output).to include("ref invalida")
63
+ end
64
+
65
+ it "trunca corpo de erro longo em BODY_MAX" do
66
+ corpo = "x" * (described_class::BODY_MAX + 500)
67
+
68
+ logging.response(:post, "https://x/v2/nfe", 500, 0.01, corpo)
69
+
70
+ expect(output).not_to include("x" * (described_class::BODY_MAX + 1))
71
+ end
72
+ end
73
+
74
+ describe "#failure" do
75
+ it "registra falha de transporte em nível ERROR com a mensagem", :aggregate_failures do
76
+ logging.failure(:get, "https://x/v2/nfe", FocusNfe::Errors::ConnectionError.new("timeout"), 0.2)
77
+
78
+ expect(output).to match(/ERROR/)
79
+ expect(output).to include("timeout")
80
+ expect(output).to include("https://x/v2/nfe")
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::HTTP::Response do
4
+ def build(status: 200, headers: {}, body: nil)
5
+ described_class.new(status: status, headers: headers, body: body)
6
+ end
7
+
8
+ describe "atributos" do
9
+ it "expõe status, headers, body e raw_body", :aggregate_failures do
10
+ response = build(status: 201, headers: { "Content-Type" => "application/json" }, body: '{"ref":"abc"}')
11
+
12
+ expect(response.status).to eq(201)
13
+ expect(response.headers["Content-Type"]).to eq("application/json")
14
+ expect(response.body).to eq("ref" => "abc")
15
+ expect(response.raw_body).to eq('{"ref":"abc"}')
16
+ end
17
+ end
18
+
19
+ describe "imutabilidade" do
20
+ it "congela a instância" do
21
+ expect(build).to be_frozen
22
+ end
23
+
24
+ it "congela o conjunto de cabeçalhos" do
25
+ response = build(headers: { "Content-Type" => "application/json" })
26
+
27
+ expect(response.headers).to be_frozen
28
+ end
29
+
30
+ it "não expõe escritores de atributos", :aggregate_failures do
31
+ response = build
32
+
33
+ expect(response).not_to respond_to(:status=)
34
+ expect(response).not_to respond_to(:body=)
35
+ end
36
+ end
37
+
38
+ describe "#success?" do
39
+ it "é verdadeiro para status 2xx", :aggregate_failures do
40
+ expect(build(status: 200)).to be_success
41
+ expect(build(status: 204)).to be_success
42
+ expect(build(status: 299)).to be_success
43
+ end
44
+
45
+ it "é falso fora da faixa 2xx", :aggregate_failures do
46
+ expect(build(status: 199)).not_to be_success
47
+ expect(build(status: 301)).not_to be_success
48
+ expect(build(status: 404)).not_to be_success
49
+ expect(build(status: 500)).not_to be_success
50
+ end
51
+ end
52
+
53
+ describe "#body" do
54
+ it "parseia JSON quando o Content-Type indica JSON" do
55
+ response = build(
56
+ headers: { "Content-Type" => "application/json; charset=utf-8" },
57
+ body: '{"status":"autorizado","itens":[1,2]}'
58
+ )
59
+
60
+ expect(response.body).to eq("status" => "autorizado", "itens" => [1, 2])
61
+ end
62
+
63
+ it "parseia arrays JSON" do
64
+ response = build(
65
+ headers: { "Content-Type" => "application/json" },
66
+ body: "[1,2,3]"
67
+ )
68
+
69
+ expect(response.body).to eq([1, 2, 3])
70
+ end
71
+
72
+ it "devolve a string crua quando não é JSON" do
73
+ response = build(
74
+ headers: { "Content-Type" => "application/xml" },
75
+ body: "<nfe>...</nfe>"
76
+ )
77
+
78
+ expect(response.body).to eq("<nfe>...</nfe>")
79
+ end
80
+
81
+ it "devolve a string crua quando não há Content-Type" do
82
+ response = build(body: "texto puro")
83
+
84
+ expect(response.body).to eq("texto puro")
85
+ end
86
+
87
+ it "cai para o corpo cru quando o JSON é inválido, sem levantar" do
88
+ response = build(
89
+ headers: { "Content-Type" => "application/json" },
90
+ body: "{invalido"
91
+ )
92
+
93
+ expect(response.body).to eq("{invalido")
94
+ end
95
+
96
+ it "devolve nil quando o corpo JSON é vazio" do
97
+ response = build(
98
+ headers: { "Content-Type" => "application/json" },
99
+ body: ""
100
+ )
101
+
102
+ expect(response.body).to be_nil
103
+ end
104
+
105
+ it "é calculado uma vez (mesmo objeto a cada leitura)" do
106
+ response = build(
107
+ headers: { "Content-Type" => "application/json" },
108
+ body: '{"ref":"abc"}'
109
+ )
110
+
111
+ expect(response.body).to equal(response.body)
112
+ end
113
+ end
114
+
115
+ describe "#raw_body" do
116
+ it "devolve sempre a string original, mesmo com JSON válido" do
117
+ response = build(
118
+ headers: { "Content-Type" => "application/json" },
119
+ body: '{"ref":"abc"}'
120
+ )
121
+
122
+ expect(response.raw_body).to eq('{"ref":"abc"}')
123
+ end
124
+ end
125
+
126
+ describe "leitura de cabeçalhos case-insensitive" do
127
+ it "encontra o cabeçalho independentemente do caso usado na busca", :aggregate_failures do
128
+ response = build(headers: { "Content-Type" => "application/json" })
129
+
130
+ expect(response.headers["content-type"]).to eq("application/json")
131
+ expect(response.headers["CONTENT-TYPE"]).to eq("application/json")
132
+ expect(response.headers["Content-Type"]).to eq("application/json")
133
+ end
134
+
135
+ it "encontra o cabeçalho independentemente do caso recebido na origem" do
136
+ response = build(headers: { "content-type" => "application/json" })
137
+
138
+ expect(response.headers["Content-Type"]).to eq("application/json")
139
+ end
140
+
141
+ it "detecta JSON mesmo quando o Content-Type chega em minúsculas" do
142
+ response = build(
143
+ headers: { "content-type" => "application/json" },
144
+ body: '{"ok":true}'
145
+ )
146
+
147
+ expect(response.body).to eq("ok" => true)
148
+ end
149
+
150
+ it "devolve nil para cabeçalho ausente" do
151
+ expect(build.headers["X-Inexistente"]).to be_nil
152
+ end
153
+
154
+ it "expõe os cabeçalhos normalizados via #to_h", :aggregate_failures do
155
+ copia = build(headers: { "Content-Type" => "application/json" }).headers.to_h
156
+
157
+ expect(copia).to eq("content-type" => "application/json")
158
+ expect(copia).not_to be_frozen
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Modelos::Documento do
4
+ def response(body:, status: 200)
5
+ FocusNfe::HTTP::Response.new(
6
+ status: status,
7
+ headers: { "Content-Type" => "application/json" },
8
+ body: JSON.generate(body)
9
+ )
10
+ end
11
+
12
+ describe ".from_response" do
13
+ it "mapeia status, status_sefaz e mensagem_sefaz", :aggregate_failures do
14
+ corpo = { "status" => "autorizado", "status_sefaz" => "100", "mensagem_sefaz" => "ok" }
15
+ doc = described_class.from_response(response(body: corpo))
16
+
17
+ expect(doc).to have_attributes(status: "autorizado", status_sefaz: "100", mensagem_sefaz: "ok")
18
+ end
19
+
20
+ it "mapeia chave, numero e serie", :aggregate_failures do
21
+ corpo = { "chave_nfe" => "3520", "numero" => "42", "serie" => "1" }
22
+ doc = described_class.from_response(response(body: corpo))
23
+
24
+ expect(doc).to have_attributes(chave_nfe: "3520", numero: "42", serie: "1")
25
+ end
26
+
27
+ it "mapeia os caminhos de XML e DANFe", :aggregate_failures do
28
+ corpo = { "caminho_xml_nota_fiscal" => "/x.xml", "caminho_danfe" => "/x.pdf" }
29
+ doc = described_class.from_response(response(body: corpo))
30
+
31
+ expect(doc).to have_attributes(caminho_xml_nota_fiscal: "/x.xml", caminho_danfe: "/x.pdf")
32
+ end
33
+
34
+ it "mapeia os campos da carta de correção", :aggregate_failures do
35
+ corpo = { "caminho_xml_carta_correcao" => "/cce.xml", "caminho_pdf_carta_correcao" => "/cce.pdf",
36
+ "numero_carta_correcao" => "1" }
37
+ doc = described_class.from_response(response(body: corpo))
38
+
39
+ expect(doc).to have_attributes(caminho_xml_carta_correcao: "/cce.xml",
40
+ caminho_pdf_carta_correcao: "/cce.pdf",
41
+ numero_carta_correcao: "1")
42
+ end
43
+
44
+ it "injeta a ref conhecida pela chamada quando o corpo não a traz" do
45
+ doc = described_class.from_response(response(body: { "status" => "processando_autorizacao" }), ref: "pedido-42")
46
+
47
+ expect(doc.ref).to eq("pedido-42")
48
+ end
49
+
50
+ it "guarda a resposta original para inspeção" do
51
+ resp = response(body: { "status" => "autorizado" })
52
+
53
+ expect(described_class.from_response(resp).response).to be(resp)
54
+ end
55
+
56
+ it "usa dados vazios quando o corpo não é um Hash", :aggregate_failures do
57
+ doc = described_class.from_response(response(body: ["x"]))
58
+
59
+ expect(doc.dados).to eq({})
60
+ expect(doc.status).to be_nil
61
+ end
62
+ end
63
+
64
+ describe ".from_payload" do
65
+ it "mapeia os campos a partir de um Hash cru", :aggregate_failures do
66
+ corpo = { "status" => "autorizado", "chave_nfe" => "3520", "numero" => "42" }
67
+ doc = described_class.from_payload(corpo)
68
+
69
+ expect(doc).to have_attributes(status: "autorizado", chave_nfe: "3520", numero: "42")
70
+ end
71
+
72
+ it "extrai a ref do próprio corpo" do
73
+ doc = described_class.from_payload({ "status" => "autorizado", "ref" => "pedido-42" })
74
+
75
+ expect(doc.ref).to eq("pedido-42")
76
+ end
77
+
78
+ it "injeta a ref informada quando o corpo não a traz" do
79
+ doc = described_class.from_payload({ "status" => "autorizado" }, ref: "pedido-42")
80
+
81
+ expect(doc.ref).to eq("pedido-42")
82
+ end
83
+
84
+ it "não guarda resposta HTTP" do
85
+ expect(described_class.from_payload({ "status" => "autorizado" }).response).to be_nil
86
+ end
87
+
88
+ it "usa dados vazios quando o corpo não é um Hash", :aggregate_failures do
89
+ doc = described_class.from_payload(["x"])
90
+
91
+ expect(doc.dados).to eq({})
92
+ expect(doc.status).to be_nil
93
+ end
94
+
95
+ it "congela a instância" do
96
+ expect(described_class.from_payload({ "status" => "autorizado" })).to be_frozen
97
+ end
98
+ end
99
+
100
+ describe "predicados de status" do
101
+ def doc(status)
102
+ described_class.from_response(response(body: { "status" => status }))
103
+ end
104
+
105
+ it "autorizado? é verdadeiro só para 'autorizado'", :aggregate_failures do
106
+ expect(doc("autorizado")).to be_autorizado
107
+ expect(doc("cancelado")).not_to be_autorizado
108
+ end
109
+
110
+ it "cancelado? é verdadeiro só para 'cancelado'", :aggregate_failures do
111
+ expect(doc("cancelado")).to be_cancelado
112
+ expect(doc("autorizado")).not_to be_cancelado
113
+ end
114
+
115
+ it "processando? é verdadeiro para 'processando_autorizacao'", :aggregate_failures do
116
+ expect(doc("processando_autorizacao")).to be_processando
117
+ expect(doc("autorizado")).not_to be_processando
118
+ end
119
+
120
+ it "erro? é verdadeiro para status que começam com 'erro'", :aggregate_failures do
121
+ expect(doc("erro_autorizacao")).to be_erro
122
+ expect(doc("autorizado")).not_to be_erro
123
+ end
124
+
125
+ it "denegado? é verdadeiro só para 'denegado'", :aggregate_failures do
126
+ expect(doc("denegado")).to be_denegado
127
+ expect(doc("autorizado")).not_to be_denegado
128
+ end
129
+ end
130
+
131
+ describe "acesso bruto" do
132
+ subject(:doc) do
133
+ described_class.from_response(response(body: { "status" => "autorizado", "extra" => "valor" }))
134
+ end
135
+
136
+ it "expõe o Hash cru via #dados" do
137
+ expect(doc.dados).to include("status" => "autorizado", "extra" => "valor")
138
+ end
139
+
140
+ it "delega #[] ao Hash para campos não mapeados" do
141
+ expect(doc["extra"]).to eq("valor")
142
+ end
143
+ end
144
+
145
+ describe "imutabilidade" do
146
+ it "congela a instância" do
147
+ expect(described_class.from_response(response(body: { "status" => "autorizado" }))).to be_frozen
148
+ end
149
+ end
150
+ end