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.
- checksums.yaml +7 -0
- data/.git-hooks/pre_push/steep.rb +18 -0
- data/.git-hooks/pre_push/yard_doc.rb +18 -0
- data/.gitattributes +1 -0
- data/.overcommit.yml +69 -0
- data/.rspec +3 -0
- data/.yardopts +11 -0
- data/CHANGELOG.md +77 -0
- data/CLAUDE.md +118 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +348 -0
- data/Rakefile +105 -0
- data/data/schemas/schema_cte.json +2793 -0
- data/data/schemas/schema_cte_os.json +1335 -0
- data/data/schemas/schema_cte_os_transporte_rodoviario.json +109 -0
- data/data/schemas/schema_cte_transporte_aereo.json +115 -0
- data/data/schemas/schema_cte_transporte_aquaviario.json +174 -0
- data/data/schemas/schema_cte_transporte_dutoviario.json +65 -0
- data/data/schemas/schema_cte_transporte_ferroviario.json +144 -0
- data/data/schemas/schema_cte_transporte_multimodal.json +45 -0
- data/data/schemas/schema_cte_transporte_rodoviario.json +78 -0
- data/data/schemas/schema_dce.json +549 -0
- data/data/schemas/schema_mdfe.json +1102 -0
- data/data/schemas/schema_mdfe_transporte_aereo.json +44 -0
- data/data/schemas/schema_mdfe_transporte_aquaviario.json +209 -0
- data/data/schemas/schema_mdfe_transporte_ferroviario.json +99 -0
- data/data/schemas/schema_mdfe_transporte_rodoviario.json +628 -0
- data/data/schemas/schema_nfcom.json +1859 -0
- data/data/schemas/schema_nfe.json +4750 -0
- data/data/schemas/schema_nfe_forma_pagamento.json +97 -0
- data/data/schemas/schema_nfe_item.json +2574 -0
- data/data/schemas/schema_nfgas.json +2316 -0
- data/data/schemas/schema_nfse_nacional.json +1847 -0
- data/data/schemas/schema_nfse_recebida.json +548 -0
- data/lib/focus_nfe/client.rb +162 -0
- data/lib/focus_nfe/configuration.rb +104 -0
- data/lib/focus_nfe/errors.rb +123 -0
- data/lib/focus_nfe/esquemas/campo.rb +171 -0
- data/lib/focus_nfe/esquemas/catalogo.rb +34 -0
- data/lib/focus_nfe/esquemas/decimal.rb +66 -0
- data/lib/focus_nfe/esquemas/esquema.rb +72 -0
- data/lib/focus_nfe/esquemas/validador.rb +87 -0
- data/lib/focus_nfe/http/adapter.rb +25 -0
- data/lib/focus_nfe/http/adapters/net_http.rb +99 -0
- data/lib/focus_nfe/http/authentication.rb +23 -0
- data/lib/focus_nfe/http/connection.rb +118 -0
- data/lib/focus_nfe/http/logging.rb +100 -0
- data/lib/focus_nfe/http/response.rb +75 -0
- data/lib/focus_nfe/modelos/documento.rb +113 -0
- data/lib/focus_nfe/modelos/inutilizacao.rb +75 -0
- data/lib/focus_nfe/modelos/pagina.rb +54 -0
- data/lib/focus_nfe/recursos/backups.rb +17 -0
- data/lib/focus_nfe/recursos/base.rb +91 -0
- data/lib/focus_nfe/recursos/ceps.rb +16 -0
- data/lib/focus_nfe/recursos/cfops.rb +16 -0
- data/lib/focus_nfe/recursos/cnaes.rb +16 -0
- data/lib/focus_nfe/recursos/cnpjs.rb +13 -0
- data/lib/focus_nfe/recursos/concerns/baixavel.rb +41 -0
- data/lib/focus_nfe/recursos/concerns/baixavel_eventos.rb +34 -0
- data/lib/focus_nfe/recursos/concerns/cancelavel.rb +25 -0
- data/lib/focus_nfe/recursos/concerns/conciliavel.rb +66 -0
- data/lib/focus_nfe/recursos/concerns/consultavel.rb +26 -0
- data/lib/focus_nfe/recursos/concerns/corrigivel.rb +45 -0
- data/lib/focus_nfe/recursos/concerns/corrigivel_cte.rb +60 -0
- data/lib/focus_nfe/recursos/concerns/emitivel.rb +51 -0
- data/lib/focus_nfe/recursos/concerns/enviavel.rb +40 -0
- data/lib/focus_nfe/recursos/concerns/eventavel.rb +46 -0
- data/lib/focus_nfe/recursos/concerns/inutilizavel.rb +96 -0
- data/lib/focus_nfe/recursos/concerns/listavel.rb +22 -0
- data/lib/focus_nfe/recursos/concerns/localizavel.rb +22 -0
- data/lib/focus_nfe/recursos/concerns/notificavel.rb +23 -0
- data/lib/focus_nfe/recursos/concerns/removivel.rb +20 -0
- data/lib/focus_nfe/recursos/concerns/visualizavel.rb +28 -0
- data/lib/focus_nfe/recursos/cte.rb +35 -0
- data/lib/focus_nfe/recursos/cte_os.rb +29 -0
- data/lib/focus_nfe/recursos/ctes_recebidas.rb +38 -0
- data/lib/focus_nfe/recursos/dce.rb +16 -0
- data/lib/focus_nfe/recursos/emails_bloqueados.rb +31 -0
- data/lib/focus_nfe/recursos/empresas.rb +35 -0
- data/lib/focus_nfe/recursos/mdfe.rb +78 -0
- data/lib/focus_nfe/recursos/municipios.rb +60 -0
- data/lib/focus_nfe/recursos/ncms.rb +16 -0
- data/lib/focus_nfe/recursos/nfce.rb +19 -0
- data/lib/focus_nfe/recursos/nfcom.rb +16 -0
- data/lib/focus_nfe/recursos/nfe.rb +106 -0
- data/lib/focus_nfe/recursos/nfes_recebidas.rb +56 -0
- data/lib/focus_nfe/recursos/nfgas.rb +16 -0
- data/lib/focus_nfe/recursos/nfse.rb +18 -0
- data/lib/focus_nfe/recursos/nfse_nacional.rb +16 -0
- data/lib/focus_nfe/recursos/nfses_nacionais_recebidas.rb +21 -0
- data/lib/focus_nfe/recursos/webhooks.rb +22 -0
- data/lib/focus_nfe/version.rb +6 -0
- data/lib/focus_nfe/webhook.rb +68 -0
- data/lib/focus_nfe.rb +124 -0
- data/sig/focus_nfe/client.rbs +38 -0
- data/sig/focus_nfe/configuration.rbs +29 -0
- data/sig/focus_nfe/errors.rbs +59 -0
- data/sig/focus_nfe/esquemas/campo.rbs +47 -0
- data/sig/focus_nfe/esquemas/catalogo.rbs +8 -0
- data/sig/focus_nfe/esquemas/decimal.rbs +25 -0
- data/sig/focus_nfe/esquemas/esquema.rbs +30 -0
- data/sig/focus_nfe/esquemas/validador.rbs +20 -0
- data/sig/focus_nfe/http/adapter.rbs +7 -0
- data/sig/focus_nfe/http/adapters/net_http.rbs +25 -0
- data/sig/focus_nfe/http/authentication.rbs +9 -0
- data/sig/focus_nfe/http/connection.rbs +32 -0
- data/sig/focus_nfe/http/logging.rbs +30 -0
- data/sig/focus_nfe/http/response.rbs +28 -0
- data/sig/focus_nfe/modelos/documento.rbs +34 -0
- data/sig/focus_nfe/modelos/inutilizacao.rbs +24 -0
- data/sig/focus_nfe/modelos/pagina.rbs +21 -0
- data/sig/focus_nfe/recursos/backups.rbs +7 -0
- data/sig/focus_nfe/recursos/base.rbs +25 -0
- data/sig/focus_nfe/recursos/ceps.rbs +10 -0
- data/sig/focus_nfe/recursos/cfops.rbs +10 -0
- data/sig/focus_nfe/recursos/cnaes.rbs +10 -0
- data/sig/focus_nfe/recursos/cnpjs.rbs +7 -0
- data/sig/focus_nfe/recursos/concerns/baixavel.rbs +12 -0
- data/sig/focus_nfe/recursos/concerns/baixavel_eventos.rbs +14 -0
- data/sig/focus_nfe/recursos/concerns/cancelavel.rbs +9 -0
- data/sig/focus_nfe/recursos/concerns/conciliavel.rbs +15 -0
- data/sig/focus_nfe/recursos/concerns/consultavel.rbs +9 -0
- data/sig/focus_nfe/recursos/concerns/corrigivel.rbs +15 -0
- data/sig/focus_nfe/recursos/concerns/corrigivel_cte.rbs +17 -0
- data/sig/focus_nfe/recursos/concerns/emitivel.rbs +14 -0
- data/sig/focus_nfe/recursos/concerns/enviavel.rbs +15 -0
- data/sig/focus_nfe/recursos/concerns/eventavel.rbs +12 -0
- data/sig/focus_nfe/recursos/concerns/inutilizavel.rbs +20 -0
- data/sig/focus_nfe/recursos/concerns/listavel.rbs +9 -0
- data/sig/focus_nfe/recursos/concerns/localizavel.rbs +9 -0
- data/sig/focus_nfe/recursos/concerns/notificavel.rbs +9 -0
- data/sig/focus_nfe/recursos/concerns/removivel.rbs +9 -0
- data/sig/focus_nfe/recursos/concerns/visualizavel.rbs +9 -0
- data/sig/focus_nfe/recursos/cte.rbs +17 -0
- data/sig/focus_nfe/recursos/cte_os.rbs +17 -0
- data/sig/focus_nfe/recursos/ctes_recebidas.rbs +14 -0
- data/sig/focus_nfe/recursos/dce.rbs +10 -0
- data/sig/focus_nfe/recursos/emails_bloqueados.rbs +12 -0
- data/sig/focus_nfe/recursos/empresas.rbs +12 -0
- data/sig/focus_nfe/recursos/mdfe.rbs +21 -0
- data/sig/focus_nfe/recursos/municipios.rbs +19 -0
- data/sig/focus_nfe/recursos/ncms.rbs +10 -0
- data/sig/focus_nfe/recursos/nfce.rbs +12 -0
- data/sig/focus_nfe/recursos/nfcom.rbs +10 -0
- data/sig/focus_nfe/recursos/nfe.rbs +23 -0
- data/sig/focus_nfe/recursos/nfes_recebidas.rbs +16 -0
- data/sig/focus_nfe/recursos/nfgas.rbs +10 -0
- data/sig/focus_nfe/recursos/nfse.rbs +11 -0
- data/sig/focus_nfe/recursos/nfse_nacional.rbs +10 -0
- data/sig/focus_nfe/recursos/nfses_nacionais_recebidas.rbs +11 -0
- data/sig/focus_nfe/recursos/webhooks.rbs +11 -0
- data/sig/focus_nfe/webhook.rbs +11 -0
- data/sig/focus_nfe.rbs +10 -0
- data/spec/focus_nfe/client_spec.rb +208 -0
- data/spec/focus_nfe/configuration_spec.rb +121 -0
- data/spec/focus_nfe/errors_mapping_spec.rb +68 -0
- data/spec/focus_nfe/errors_spec.rb +107 -0
- data/spec/focus_nfe/esquemas/campo_spec.rb +291 -0
- data/spec/focus_nfe/esquemas/decimal_spec.rb +54 -0
- data/spec/focus_nfe/esquemas/esquema_spec.rb +73 -0
- data/spec/focus_nfe/esquemas/validador_spec.rb +167 -0
- data/spec/focus_nfe/esquemas_spec.rb +42 -0
- data/spec/focus_nfe/http/adapter_spec.rb +8 -0
- data/spec/focus_nfe/http/adapters/net_http_spec.rb +181 -0
- data/spec/focus_nfe/http/authentication_spec.rb +24 -0
- data/spec/focus_nfe/http/connection_spec.rb +255 -0
- data/spec/focus_nfe/http/logging_spec.rb +83 -0
- data/spec/focus_nfe/http/response_spec.rb +161 -0
- data/spec/focus_nfe/modelos/documento_spec.rb +150 -0
- data/spec/focus_nfe/modelos/inutilizacao_spec.rb +91 -0
- data/spec/focus_nfe/modelos/pagina_spec.rb +77 -0
- data/spec/focus_nfe/recursos/backups_spec.rb +29 -0
- data/spec/focus_nfe/recursos/base_spec.rb +56 -0
- data/spec/focus_nfe/recursos/ceps_spec.rb +16 -0
- data/spec/focus_nfe/recursos/cfops_spec.rb +16 -0
- data/spec/focus_nfe/recursos/cnaes_spec.rb +20 -0
- data/spec/focus_nfe/recursos/cnpjs_spec.rb +11 -0
- data/spec/focus_nfe/recursos/concerns/emitivel_validacao_spec.rb +158 -0
- data/spec/focus_nfe/recursos/cte_os_spec.rb +9 -0
- data/spec/focus_nfe/recursos/cte_spec.rb +9 -0
- data/spec/focus_nfe/recursos/ctes_recebidas_spec.rb +56 -0
- data/spec/focus_nfe/recursos/dce_spec.rb +8 -0
- data/spec/focus_nfe/recursos/emails_bloqueados_spec.rb +29 -0
- data/spec/focus_nfe/recursos/empresas_spec.rb +45 -0
- data/spec/focus_nfe/recursos/mdfe_spec.rb +100 -0
- data/spec/focus_nfe/recursos/municipios_spec.rb +58 -0
- data/spec/focus_nfe/recursos/ncms_spec.rb +16 -0
- data/spec/focus_nfe/recursos/nfce_spec.rb +10 -0
- data/spec/focus_nfe/recursos/nfcom_spec.rb +8 -0
- data/spec/focus_nfe/recursos/nfe_spec.rb +262 -0
- data/spec/focus_nfe/recursos/nfes_recebidas_spec.rb +87 -0
- data/spec/focus_nfe/recursos/nfgas_spec.rb +8 -0
- data/spec/focus_nfe/recursos/nfse_nacional_spec.rb +8 -0
- data/spec/focus_nfe/recursos/nfse_spec.rb +9 -0
- data/spec/focus_nfe/recursos/nfses_nacionais_recebidas_spec.rb +17 -0
- data/spec/focus_nfe/recursos/webhooks_spec.rb +22 -0
- data/spec/focus_nfe/webhook_spec.rb +66 -0
- data/spec/focus_nfe_global_configuration_spec.rb +70 -0
- data/spec/focus_nfe_require_spec.rb +87 -0
- data/spec/focus_nfe_spec.rb +11 -0
- data/spec/spec_helper.rb +58 -0
- data/spec/support/shared_examples/recurso_fiscal.rb +445 -0
- data/spec/support/shared_examples/recurso_leitura.rb +217 -0
- data/tools/pull_fields.rb +62 -0
- 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
|