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,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Esquemas::Esquema do
4
+ describe ".carregar" do
5
+ it "carrega o schema empacotado de um documento conhecido", :aggregate_failures do
6
+ esquema = described_class.carregar("nfe")
7
+
8
+ expect(esquema).to be_a(described_class)
9
+ expect(esquema.campos).to all(be_a(FocusNfe::Esquemas::Campo))
10
+ expect(esquema.campos).not_to be_empty
11
+ end
12
+
13
+ it "devolve nil quando não há schema para o documento" do
14
+ expect(described_class.carregar("nfce")).to be_nil
15
+ end
16
+
17
+ it "memoiza o schema carregado" do
18
+ expect(described_class.carregar("nfe")).to equal(described_class.carregar("nfe"))
19
+ end
20
+
21
+ it "devolve nil para nomes inválidos (traversal)", :aggregate_failures do
22
+ expect(described_class.carregar("../../../../etc/hostname")).to be_nil
23
+ expect(described_class.carregar("nfe/../nfe")).to be_nil
24
+ expect(described_class.carregar("../secret")).to be_nil
25
+ end
26
+
27
+ it "não consulta o filesystem para nomes inválidos" do
28
+ allow(File).to receive(:exist?).and_call_original
29
+
30
+ described_class.carregar("../../../../etc/passwd")
31
+
32
+ expect(File).not_to have_received(:exist?)
33
+ end
34
+
35
+ it "não memoiza nomes inválidos (cache permanece limitado)" do
36
+ described_class.carregar("../etc/hostname")
37
+
38
+ expect(described_class.send(:cache)).not_to have_key("../etc/hostname")
39
+ end
40
+ end
41
+
42
+ describe "#campos" do
43
+ let(:definicoes) do
44
+ [
45
+ { "name" => "serie", "type" => "String[1-3]", "required" => true },
46
+ { "name" => "numero", "type" => "Integer[1-9]", "required" => true }
47
+ ]
48
+ end
49
+
50
+ it "constrói Campos a partir das definições in-memory", :aggregate_failures do
51
+ esquema = described_class.new(definicoes)
52
+
53
+ expect(esquema.campos.map(&:nome)).to eq(%w[serie numero])
54
+ expect(esquema.campos.first).to be_obrigatorio
55
+ end
56
+ end
57
+
58
+ describe "#descrever" do
59
+ let(:definicoes) do
60
+ [
61
+ { "name" => "serie", "type" => "String[1-3]", "required" => true },
62
+ { "name" => "numero", "type" => "Integer[1-9]", "required" => true }
63
+ ]
64
+ end
65
+
66
+ it "descreve cada campo como um Hash", :aggregate_failures do
67
+ descricao = described_class.new(definicoes).descrever
68
+
69
+ expect(descricao).to eq(definicoes.map { |d| FocusNfe::Esquemas::Campo.new(d).to_h })
70
+ expect(descricao.map { |c| c[:nome] }).to eq(%w[serie numero])
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Esquemas::Validador do
4
+ subject(:validador) { described_class.new(esquema) }
5
+
6
+ let(:esquema) do
7
+ FocusNfe::Esquemas::Esquema.new(
8
+ [
9
+ { "name" => "natureza_operacao", "type" => "String[1-60]", "required" => true },
10
+ { "name" => "serie", "type" => "String[1-3]", "required" => false },
11
+ { "name" => "numero", "type" => "Integer[1-9]", "required" => false },
12
+ { "name" => "items", "type" => "Coleção[1-990]", "required" => true,
13
+ "collection" => { "object_attributes" => [valor_obrigatorio] } }
14
+ ]
15
+ )
16
+ end
17
+
18
+ let(:valor_obrigatorio) { { "name" => "valor", "type" => "Decimal[13.2]", "required" => true } }
19
+
20
+ describe "#validar" do
21
+ it "acusa campo obrigatório ausente" do
22
+ erros = validador.validar("serie" => "1")
23
+
24
+ expect(erros.join).to include("natureza_operacao")
25
+ end
26
+
27
+ it "acusa string acima do tamanho máximo" do
28
+ erros = validador.validar("natureza_operacao" => "Venda", "serie" => "1234", "items" => [])
29
+
30
+ expect(erros.join).to include("serie")
31
+ end
32
+
33
+ it "não acusa nada para um payload válido" do
34
+ erros = validador.validar("natureza_operacao" => "Venda", "items" => [{ "valor" => "10.00" }])
35
+
36
+ expect(erros).to eq([])
37
+ end
38
+
39
+ it "propaga erro de decimal de dentro de uma coleção" do
40
+ erros = validador.validar("natureza_operacao" => "Venda", "items" => [{ "valor" => "10.123" }])
41
+
42
+ expect(erros.join).to include("items[1].valor")
43
+ end
44
+
45
+ it "propaga erro de enum de um campo de topo" do
46
+ esquema = FocusNfe::Esquemas::Esquema.new(
47
+ [{ "name" => "indicador", "type" => nil, "enum" => "* +0+: Não\\n* +1+: Sim", "required" => true }]
48
+ )
49
+
50
+ expect(described_class.new(esquema).validar("indicador" => "9").join).to include("indicador")
51
+ end
52
+
53
+ it "aceita chaves String e Symbol no payload", :aggregate_failures do
54
+ expect(validador.validar(natureza_operacao: "Venda", items: [])).to eq([])
55
+ expect(validador.validar("natureza_operacao" => "Venda", "items" => [])).to eq([])
56
+ end
57
+ end
58
+
59
+ describe "#validar!" do
60
+ it "levanta ErroDeValidacao com a lista de erros", :aggregate_failures do
61
+ expect { validador.validar!("serie" => "1") }.to raise_error(FocusNfe::Esquemas::ErroDeValidacao) do |erro|
62
+ expect(erro.erros.join).to include("natureza_operacao")
63
+ expect(erro.erros.join).to include("items")
64
+ end
65
+ end
66
+
67
+ it "não levanta para payload válido" do
68
+ expect { validador.validar!("natureza_operacao" => "Venda", "items" => []) }.not_to raise_error
69
+ end
70
+ end
71
+
72
+ describe "#validar recorrendo em coleções" do
73
+ subject(:validador) { described_class.new(esquema) }
74
+
75
+ let(:esquema) do
76
+ FocusNfe::Esquemas::Esquema.new(
77
+ [
78
+ { "name" => "itens", "type" => "Coleção[1-990]", "required" => true,
79
+ "collection" => { "object_attributes" => [
80
+ { "name" => "descricao", "type" => "String[1-5]", "required" => true },
81
+ { "name" => "adicoes", "type" => "Coleção[0-99]", "required" => false,
82
+ "collection" => { "object_attributes" => [
83
+ { "name" => "numero", "type" => "Integer[1-3]", "required" => true }
84
+ ] } }
85
+ ] } }
86
+ ]
87
+ )
88
+ end
89
+
90
+ it "acusa subcampo obrigatório ausente dentro do item" do
91
+ erros = validador.validar("itens" => [{}])
92
+
93
+ expect(erros.join).to include("itens[1].descricao", "campo obrigatório ausente")
94
+ end
95
+
96
+ it "valida tipo/tamanho do subcampo dentro do item" do
97
+ erros = validador.validar("itens" => [{ "descricao" => "longa demais" }])
98
+
99
+ expect(erros.join).to include("itens[1].descricao")
100
+ end
101
+
102
+ it "usa a posição (base 1) no prefixo do erro" do
103
+ erros = validador.validar("itens" => [{ "descricao" => "ok" }, {}])
104
+
105
+ expect(erros.join).to include("itens[2].descricao")
106
+ end
107
+
108
+ it "acusa coleção que não é Array" do
109
+ expect(validador.validar("itens" => "x").join).to include("itens: deve ser uma coleção")
110
+ end
111
+
112
+ it "acusa item que não é objeto" do
113
+ expect(validador.validar("itens" => ["x"]).join).to include("itens[1]: deve ser um objeto")
114
+ end
115
+
116
+ it "recorre em coleções aninhadas em qualquer profundidade" do
117
+ erros = validador.validar("itens" => [{ "descricao" => "ok", "adicoes" => [{}] }])
118
+
119
+ expect(erros.join).to include("itens[1].adicoes[1].numero")
120
+ end
121
+
122
+ it "não acusa nada quando todos os itens são válidos" do
123
+ dados = { "itens" => [{ "descricao" => "ok", "adicoes" => [{ "numero" => "12" }] }] }
124
+
125
+ expect(validador.validar(dados)).to eq([])
126
+ end
127
+
128
+ it "não restringe o conteúdo de coleção sem object_attributes" do
129
+ esquema = FocusNfe::Esquemas::Esquema.new(
130
+ [{ "name" => "anexos", "type" => "Coleção[0-9]", "collection" => {} }]
131
+ )
132
+
133
+ expect(described_class.new(esquema).validar("anexos" => [{ "x" => "y" }])).to eq([])
134
+ end
135
+ end
136
+
137
+ describe "#validar com sub-esquemas aninhados" do
138
+ subject(:validador) { described_class.new(esquema_topo, aninhados: { "modal_rodoviario" => sub_esquema }) }
139
+
140
+ let(:esquema_topo) { FocusNfe::Esquemas::Esquema.new([]) }
141
+ let(:sub_esquema) do
142
+ FocusNfe::Esquemas::Esquema.new([{ "name" => "rntrc", "type" => "String[8]", "required" => true }])
143
+ end
144
+
145
+ it "não acusa nada quando o objeto aninhado é válido" do
146
+ expect(validador.validar("modal_rodoviario" => { "rntrc" => "12345678" })).to eq([])
147
+ end
148
+
149
+ it "valida o objeto aninhado com chaves Symbol" do
150
+ expect(validador.validar(modal_rodoviario: { rntrc: "12345678" })).to eq([])
151
+ end
152
+
153
+ it "prefixa os erros do aninhado com a chave" do
154
+ erros = validador.validar("modal_rodoviario" => { "rntrc" => "1" })
155
+
156
+ expect(erros.join).to include("modal_rodoviario.rntrc")
157
+ end
158
+
159
+ it "exige o objeto aninhado declarado" do
160
+ expect(validador.validar({}).join).to include("modal_rodoviario: campo obrigatório ausente")
161
+ end
162
+
163
+ it "acusa quando o aninhado não é um objeto" do
164
+ expect(validador.validar("modal_rodoviario" => "x").join).to include("modal_rodoviario: deve ser um objeto")
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Esquemas do
4
+ describe ".disponiveis" do
5
+ it "lista os nomes dos documentos com schema empacotado", :aggregate_failures do
6
+ disponiveis = described_class.disponiveis
7
+
8
+ expect(disponiveis).to be_an(Array)
9
+ expect(disponiveis).to all(be_a(String))
10
+ expect(disponiveis).to include("nfe")
11
+ end
12
+
13
+ it "devolve os nomes ordenados e sem duplicatas", :aggregate_failures do
14
+ disponiveis = described_class.disponiveis
15
+
16
+ expect(disponiveis).to eq(disponiveis.uniq)
17
+ expect(disponiveis).to eq(disponiveis.sort)
18
+ end
19
+ end
20
+
21
+ describe ".descrever" do
22
+ it "devolve a descrição estruturada de cada campo do documento", :aggregate_failures do
23
+ campos = described_class.descrever("nfe")
24
+
25
+ expect(campos).to be_an(Array)
26
+ expect(campos).not_to be_empty
27
+ expect(campos.first).to include(:nome, :tipo, :obrigatorio)
28
+ end
29
+
30
+ it "devolve nil quando não há schema para o documento" do
31
+ expect(described_class.descrever("documento_inexistente")).to be_nil
32
+ end
33
+
34
+ it "devolve nil para nomes inválidos (traversal)" do
35
+ expect(described_class.descrever("../../foo")).to be_nil
36
+ end
37
+
38
+ it "expõe os campos sem exigir token nem conexão" do
39
+ expect { described_class.descrever("nfe") }.not_to raise_error
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::HTTP::Adapter do
4
+ it "define #call como interface abstrata, levantando NotImplementedError" do
5
+ expect { described_class.new.call(:get, "https://exemplo.test/x") }
6
+ .to raise_error(NotImplementedError)
7
+ end
8
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::HTTP::Adapters::NetHttp do
4
+ subject(:adapter) { described_class.new(timeout: 30, open_timeout: 10) }
5
+
6
+ let(:url) { "https://api.exemplo.test/recurso" }
7
+ let(:json) { { "Content-Type" => "application/json" } }
8
+
9
+ def capture_http
10
+ http = nil
11
+ allow(Net::HTTP).to receive(:new).and_wrap_original do |original, *args|
12
+ http = original.call(*args)
13
+ end
14
+ yield
15
+ http
16
+ end
17
+
18
+ def bytes_escritos(request)
19
+ escrito = +""
20
+ sock = Object.new
21
+ sock.define_singleton_method(:write) do |data|
22
+ escrito << data.to_s
23
+ data.to_s.bytesize
24
+ end
25
+ sock.define_singleton_method(:continue_timeout) { nil }
26
+ request.exec(sock, "1.1", "/recurso")
27
+ escrito
28
+ end
29
+
30
+ it "é um Adapter" do
31
+ expect(adapter).to be_a(FocusNfe::HTTP::Adapter)
32
+ end
33
+
34
+ describe "#call" do
35
+ it "devolve uma Response com status e corpo parseado", :aggregate_failures do
36
+ stub_request(:get, url).to_return(status: 200, body: '{"ok":true}', headers: json)
37
+
38
+ response = adapter.call(:get, url)
39
+
40
+ expect(response).to be_a(FocusNfe::HTTP::Response)
41
+ expect(response).to have_attributes(status: 200, body: { "ok" => true })
42
+ end
43
+
44
+ %i[get post put delete].each do |verb|
45
+ it "suporta o verbo #{verb}" do
46
+ stub = stub_request(verb, url).to_return(status: 200, body: "")
47
+
48
+ adapter.call(verb, url)
49
+
50
+ expect(stub).to have_been_requested
51
+ end
52
+ end
53
+
54
+ it "envia o corpo no POST" do
55
+ stub = stub_request(:post, url).with(body: '{"a":1}').to_return(status: 201, body: "")
56
+
57
+ adapter.call(:post, url, headers: json, body: '{"a":1}')
58
+
59
+ expect(stub).to have_been_requested
60
+ end
61
+
62
+ it "envia o corpo no DELETE (cancelamento com justificativa)" do
63
+ stub = stub_request(:delete, url).with(body: '{"justificativa":"erro"}').to_return(status: 200, body: "")
64
+
65
+ adapter.call(:delete, url, body: '{"justificativa":"erro"}')
66
+
67
+ expect(stub).to have_been_requested
68
+ end
69
+
70
+ it "de fato grava corpo e Content-Length ao serializar um Net::HTTP::Delete", :aggregate_failures do
71
+ request = adapter.send(:build_request, :delete, URI(url), json, '{"tipo_evento":"x"}')
72
+ escrito = bytes_escritos(request)
73
+
74
+ expect(escrito).to include('{"tipo_evento":"x"}')
75
+ expect(escrito).to match(/content-length: 19/i)
76
+ end
77
+
78
+ it "respeita um Content-Type não-JSON no corpo da requisição" do
79
+ headers = { "Content-Type" => "application/xml" }
80
+ stub = stub_request(:post, url).with(body: "<x/>", headers: headers).to_return(status: 200, body: "")
81
+
82
+ adapter.call(:post, url, headers: headers, body: "<x/>")
83
+
84
+ expect(stub).to have_been_requested
85
+ end
86
+
87
+ it "repassa corpo de resposta não-JSON cru, sem estourar" do
88
+ headers = { "Content-Type" => "application/xml" }
89
+ stub_request(:get, url).to_return(status: 200, body: "<nfe>...</nfe>", headers: headers)
90
+
91
+ expect(adapter.call(:get, url).body).to eq("<nfe>...</nfe>")
92
+ end
93
+
94
+ it "aplica timeout e open_timeout ao cliente Net::HTTP", :aggregate_failures do
95
+ stub_request(:get, url).to_return(status: 200, body: "")
96
+
97
+ http = capture_http { described_class.new(timeout: 7, open_timeout: 3).call(:get, url) }
98
+
99
+ expect(http).to have_attributes(read_timeout: 7, open_timeout: 3)
100
+ end
101
+
102
+ it "não força timeouts quando não configurados, mantendo os defaults do Net::HTTP", :aggregate_failures do
103
+ stub_request(:get, url).to_return(status: 200, body: "")
104
+ padrao = Net::HTTP.new("x")
105
+
106
+ http = capture_http { described_class.new.call(:get, url) }
107
+
108
+ expect(http.read_timeout).to eq(padrao.read_timeout)
109
+ expect(http.open_timeout).to eq(padrao.open_timeout)
110
+ end
111
+
112
+ it "usa TLS quando a URL é https" do
113
+ stub_request(:get, url).to_return(status: 200, body: "")
114
+
115
+ http = capture_http { adapter.call(:get, url) }
116
+
117
+ expect(http.use_ssl?).to be(true)
118
+ end
119
+ end
120
+
121
+ describe "redirecionamento 302" do
122
+ let(:origin) { "https://api.exemplo.test/nfe/123.pdf" }
123
+ let(:target) { "https://arquivos.exemplo.test/assinado/123.pdf" }
124
+
125
+ def stub_redirect
126
+ stub_request(:get, origin).to_return(status: 302, headers: { "Location" => target })
127
+ stub_request(:get, target).to_return(status: 200, body: "PDF")
128
+ end
129
+
130
+ it "segue o Location com um novo GET e devolve a resposta final" do
131
+ stub_redirect
132
+
133
+ response = adapter.call(:get, origin, headers: { "Authorization" => "Basic abc" })
134
+
135
+ expect(response.status).to eq(200)
136
+ end
137
+
138
+ it "não reenvia o cabeçalho Authorization ao seguir o 302" do
139
+ stub_redirect
140
+
141
+ adapter.call(:get, origin, headers: { "Authorization" => "Basic abc" })
142
+
143
+ expect(a_request(:get, target).with { |req| req.headers["Authorization"].nil? }).to have_been_made
144
+ end
145
+
146
+ it "recusa redirecionamento para destino não-https", :aggregate_failures do
147
+ destino_http = "http://arquivos.exemplo.test/assinado/123.pdf"
148
+ stub_request(:get, origin).to_return(status: 302, headers: { "Location" => destino_http })
149
+ segundo = stub_request(:get, destino_http)
150
+
151
+ expect { adapter.call(:get, origin) }.to raise_error(FocusNfe::Errors::ConnectionError, /não-https/)
152
+ expect(segundo).not_to have_been_requested
153
+ end
154
+
155
+ it "não segue outros 3xx além de 302 (devolve a resposta crua)" do
156
+ stub_request(:get, url).to_return(status: 301, headers: { "Location" => "https://outro.test/x" })
157
+
158
+ expect(adapter.call(:get, url).status).to eq(301)
159
+ end
160
+
161
+ it "levanta ConnectionError ao exceder o teto de 5 redirecionamentos" do
162
+ stub_request(:get, url).to_return(status: 302, headers: { "Location" => url })
163
+
164
+ expect { adapter.call(:get, url) }.to raise_error(FocusNfe::Errors::ConnectionError)
165
+ end
166
+ end
167
+
168
+ describe "falhas de transporte" do
169
+ it "relança timeout como ConnectionError" do
170
+ stub_request(:get, url).to_timeout
171
+
172
+ expect { adapter.call(:get, url) }.to raise_error(FocusNfe::Errors::ConnectionError)
173
+ end
174
+
175
+ it "relança conexão recusada como ConnectionError" do
176
+ stub_request(:get, url).to_raise(Errno::ECONNREFUSED)
177
+
178
+ expect { adapter.call(:get, url) }.to raise_error(FocusNfe::Errors::ConnectionError)
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::HTTP::Authentication do
4
+ it "produz o par { \"Authorization\" => \"Basic <base64>\" }", :aggregate_failures do
5
+ header = described_class.header("abc")
6
+
7
+ expect(header.keys).to eq(["Authorization"])
8
+ expect(header["Authorization"]).to start_with("Basic ")
9
+ end
10
+
11
+ it "decodifica de volta para 'token:' — token como usuário, senha vazia", :aggregate_failures do
12
+ value = described_class.header("meu-token").fetch("Authorization")
13
+ decoded = value.delete_prefix("Basic ").unpack1("m0")
14
+
15
+ expect(decoded).to eq("meu-token:")
16
+ expect(decoded).to end_with(":")
17
+ end
18
+
19
+ it "gera Base64 sem quebra de linha (equivalente exato a strict_encode64)" do
20
+ value = described_class.header("x").fetch("Authorization")
21
+
22
+ expect(value).to eq("Basic eDo=")
23
+ end
24
+ end