cypress-dfe
v0.2.1
Published
Gera NF-e, CT-e e MDF-e para teste automatizado — e corrompe de propósito, de um jeito nomeado, para você provar que seu sistema recusa documento fiscal inválido. Não emite e não transmite.
Maintainers
Readme
cypress-dfe
Gera NF-e (55), CT-e (57) e MDF-e (58) nos seus testes de Cypress — e corrompe de propósito, para você provar que o seu sistema recusa documento fiscal inválido.
npm i -D cypress-dfeConfigurar — duas linhas
cypress.config.js:
const { defineConfig } = require('cypress')
const { registerDFeTasks } = require('cypress-dfe/tasks')
module.exports = defineConfig({
e2e: {
setupNodeEvents(on) {
registerDFeTasks(on) // ← aqui
},
},
})cypress/support/e2e.js:
import 'cypress-dfe/commands' // ← e aquiPronto. Os comandos cy.createNFe(), cy.corruptDFe() e companhia já existem,
com autocomplete e tipos — sem precisar de .d.ts nenhum.
Seu primeiro teste
Cole isso em cypress/e2e/fiscal.cy.js e rode:
describe('import de NF-e', () => {
it('aceita uma nota válida', () => {
cy.createNFe({ uf: 'PB', CNPJ: '11222333000181' }).then(({ xml, accessKey }) => {
cy.request({
method: 'POST',
url: '/api/notas/import',
body: { conteudo_xml: xml },
failOnStatusCode: false,
}).then(({ status, body }) => {
expect(status).to.eq(201)
expect(body.chave).to.eq(accessKey)
})
})
})
it('recusa uma nota com a chave adulterada', () => {
cy.createNFe({ uf: 'PB', CNPJ: '11222333000181' })
.corruptDFe('checkDigit')
.then(({ xml }) => {
cy.request({
method: 'POST',
url: '/api/notas/import',
body: { conteudo_xml: xml },
failOnStatusCode: false,
})
.its('status')
.should('eq', 400)
})
})
})Todo comando devolve { xml, accessKey, model }. O xml é o que você manda
para o seu sistema; o accessKey é a chave de 44 dígitos para conferir o que
ele gravou.
Gerando o documento
cy.createNFe(opcoes) // modelo 55
cy.createCTe(opcoes) // modelo 57
cy.createMDFe(opcoes) // modelo 58
cy.createDFe({ model: '57', ...opcoes })Só uf e CNPJ são obrigatórios. Todo o resto tem padrão.
Identificação
| opção | padrão | |
|---|---|---|
| uf | — | sigla do emitente: 'PB', 'SP' |
| CNPJ | — | 14 dígitos, sem pontuação |
| numero | '1' | número do documento |
| serie | '1' | série |
| dhEmi | '2026-01-01T00:00:00-03:00' | emissão, com fuso obrigatório |
| tpAmb | '2' | 2 homologação, 1 produção (gera aviso) |
| seed | 0 | semente do sorteio do código numérico |
| codigo | sorteado | código numérico de 8 dígitos |
Valores, peso e itens
É o que move cálculo de frete no seu sistema.
| opção | padrão | |
|---|---|---|
| valor | 10 | total do documento |
| itens | um item genérico | lista de produtos |
| pesoBruto | 1 | kg |
| pesoLiquido | igual ao bruto | kg |
| volumes | 1 | contagem; null omite a tag |
| valorCarga | igual a valor | CT-e e MDF-e |
| produtoPredominante | 'PRODUTO DE TESTE' | CT-e |
cy.createNFe({
uf: 'PB',
CNPJ: '11222333000181',
pesoBruto: 432.25,
volumes: 7,
itens: [
{ descricao: 'CAIXA', quantidade: 4, valorUnitario: 25, ncm: '39269090' },
{ descricao: 'PALETE', quantidade: 2, valorUnitario: 100 },
],
})
// vNF sai 300.00 — a soma dos itensCada item aceita descricao, quantidade, valorUnitario, codigo, ncm,
cfop, unidade e ean.
Se você passar itens, o total é a soma deles. Passar também um valor
que não bate é erro — um documento que se contradiz seria recusado pelo seu
sistema por um motivo que não é o que você queria testar.
Para testar nota que não declara volume:
cy.createNFe({ uf: 'PB', CNPJ: '11222333000181', volumes: null })Documentos referenciados
O CT-e transporta NF-e; o MDF-e manifesta CT-e. Já sai referenciado, e o totalizador bate:
cy.createMDFe({ uf: 'PB', CNPJ: '11222333000181' })
// <qCTe>1</qCTe> e um <chCTe> com chave válida de CT-ePara amarrar em documentos seus:
cy.createNFe(emitente).then(({ accessKey }) => {
cy.createCTe({ ...emitente, chavesNfe: [accessKey] })
})
cy.createMDFe({ ...emitente, chavesCte: [chaveDoCte, outraChave] })Alterar × corromper
Esta é a distinção que mais importa no pacote.
| | valida? | recalcula a chave? |
|---|---|---|
| setDFeField(caminho, valor) | sim | sim |
| corruptDFe*(...) | não | não |
setDFeField é para mudar algo legítimo. Mexeu em campo que compõe a chave? A
chave é refeita, e o Id junto:
cy.createNFe(emitente)
.setDFeField('ide.nNF', '9999') // a chave muda sozinha
.setDFeField('transp.modFrete', '0') // campo que não compõe a chaveEle recusa valor inválido — ide.dhEmi com mês 13 lança erro citando o
campo e a regra. Isso é proposital: recusar é o trabalho dele.
As corrupções fazem o oposto, e é por isso que existem separadas. Nada fica inválido por engano — só por uma chamada que diz isso no nome.
Corrompendo
Um comando aceita qualquer defeito do catálogo:
cy.createNFe(emitente).corruptDFe('checkDigit')E o catálogo é iterável — é assim que você gera a suíte de rejeição inteira:
import { allCorruptions } from 'cypress-dfe'
describe('meu sistema recusa documento inválido', () => {
for (const c of allCorruptions('55')) {
it(`recusa ${c.title}`, () => {
cy.createNFe(emitente)
.corruptDFe(c.id)
.then(({ xml }) => {
cy.request({ method: 'POST', url: '/api/notas/import', body: { conteudo_xml: xml }, failOnStatusCode: false })
.its('status')
.should('eq', 400)
})
})
}
})São 28 testes na NF-e e 32 no MDF-e, cada um com title, rule e
expects — use o expects como mensagem da asserção e o relatório do teste
passa a dizer o que se esperava.
Os defeitos disponíveis
Chave de acesso (CORRUPTIONS)
| | |
|---|---|
| checkDigit | dígito verificador não fecha |
| tooShort · tooLong | 43 e 45 dígitos |
| nonNumeric | letra no meio |
| unknownUF · monthOutOfRange · unknownModel | UF 99, mês 13, modelo 99 |
| otherModel · otherIssuer · otherNumber | chave impecável, de outro documento |
Data de emissão (FIELD_CORRUPTIONS)
| | |
|---|---|
| issuedAtWithZ | sufixo Z — o erro de quem usa toISOString() |
| issuedAtWithoutTimezone · issuedAtMonth13 · issuedAtDay32 | |
| issuedAtEmpty · issuedAtNotADate | |
| issuedAtInFuture · issuedAtInPast | 2099 e 2000 |
Campo ausente (missingFieldCorruptions(modelo))
missingNumber, missingSerie, missingCode, missingIssuedAt,
missingModel, missingUF, missingIssuerCNPJ, missingIssuerName,
missingAccessKey, missingTotal.
Remover é diferente de esvaziar: <dhEmi/> existe e está vazio; sem a tag, o
campo não existe. Seu parser pode tratar cada caso de um jeito.
Transporte (transportCorruptions(modelo)) — a validação cruzada que só um
TMS faz:
manifestCountTooHigh, manifestCountTooLow, manifestWithoutDocuments,
manifestKeyBrokenCheckDigit, manifestKeyWrongModel, freightTotalMismatch,
freightWithoutComponents, cargoKeyBrokenCheckDigit, cargoKeyWrongModel,
cargoWithoutDocuments.
Corromper algo que não está no catálogo
cy.createNFe(emitente).corruptDFeRaw('total.ICMSTot.vNF', '-500.00') // valor cru
cy.createNFe(emitente).corruptDFeRemove('emit.xNome') // remove o campoO caminho é o do XML a partir do grupo assinado: ide.nNF, emit.CNPJ,
total.ICMSTot.vNF, transp.vol. Para atributo, use @: infNFe@Id.
Por que a chave não é recalculada ao corromper
Porque a divergência é o produto. corruptDFeRaw('ide.nNF', '9999') deixa o
Id dizendo 1234 e o corpo dizendo 9999 — que é o que um XML adulterado
parece, e o que o seu sistema tem que perceber.
Partindo de um XML seu
Se você já tem NF-e de verdade, não precisa gerar:
cy.loadDFe('cypress/fixtures/nfe-real.xml')
.corruptDFe('otherIssuer')
.then(({ xml }) => { /* ... */ })A estrutura vem do seu arquivo; a chave e as corrupções funcionam por cima.
Outros comandos
cy.createNFe(emitente).saveDFe('cypress/downloads/nota.xml') // grava em disco
cy.createNFe(emitente).dfeAccessKey() // só a chave
cy.nextDFeNumber('1') // numeração sem colisãoPara upload em tela, transforme o XML em arquivo sem passar pelo disco:
cy.createNFe(emitente).then(({ xml }) => {
cy.get('input[type=file]').selectFile(
{ contents: Cypress.Buffer.from(xml), fileName: 'nota.xml', mimeType: 'text/xml' },
{ force: true },
)
})Por que meu segundo teste dá 409
Porque o gerador é determinístico: mesma entrada, mesma chave. E a chave é única em âmbito nacional — então a segunda importação do mesmo documento é recusada por duplicidade, não pelo defeito que você queria testar.
O sintoma engana: o primeiro teste passa e todos os seguintes falham com um status sem relação com o que está sob teste.
Varie o número:
const BASE = Number(String(Date.now()).slice(-8, -2))
let n = 0
const emitente = () => ({
uf: 'PB',
CNPJ: '11222333000181',
numero: String(BASE + (n += 1)),
})
cy.createNFe(emitente()).corruptDFe('checkDigit')A base vem do relógio porque o banco do seu sistema persiste entre execuções.
Dentro de uma execução só, cy.nextDFeNumber('1') resolve.
O determinismo fica porque é ele que faz um teste que falhou na CI se reproduzir na sua máquina.
Fora do Cypress
O núcleo é puro — sem fs, sem XML, sem Cypress. Roda em Vitest, Jest,
Playwright ou script solto:
import { buildAccessKey, validateAccessKey, corruptAccessKey } from 'cypress-dfe'
import { createNFe, corruptDocument } from 'cypress-dfe/node'
const doc = createNFe({ uf: 'PB', CNPJ: '11222333000181', valor: 500 })
doc.accessKey()
doc.toXML()
corruptDocument(doc.toXML(), 'missingNumber').xml
validateAccessKey(doc.accessKey(), { model: '55' }) // { valid: true, errors: [] }Nome de campo fiscal não é traduzido: nNF, cNF, dhEmi, cUF e tpAmb
seguem o MOC. As mensagens de erro são em português e citam campo e regra.
Escopo
- NF-e (55), CT-e (57) e MDF-e (58) — o recorte é transporte de carga
- Não emite e não transmite — não fala com a SEFAZ
- Não calcula tributo — não apura ICMS nem valida CST
- Não é para produção — padrão é homologação, sem valor fiscal
Estado, sem maquiagem:
| | |
|---|---|
| Chave de acesso nos três modelos | ✅ com vetor de ouro do MOC do MDF-e |
| Corrupção — 4 famílias | ✅ autoverificadas |
| Template NF-e 55 | ✅ ordem conferida no leiaute 4.00 |
| Template CT-e 57 e MDF-e 58 | ⚠️ ordem do ide ainda não validada contra o XSD |
| Assinatura A1 · validação XSD | ⏳ |
Precisa de CT-e fiel ao leiaute hoje? Use cy.loadDFe() com um XML seu.
Mexer no plugin
git clone https://github.com/logangaabriel/cypress-dfe
cd cypress-dfe && npm install
npm run verificar # tipos + lint + testes com cobertura + auditoria
npm test # só os testes
npx cypress run # smoke dos comandos
npm run buildO projeto é governado pelo REGRAS.md, um documento normativo com
identificador estável por regra (RD-03, RF-09, ESC-02), citado em code
review, commit e mensagem de erro.
Regra escrita de memória é marcada ⚠️ e proibida de implementar antes de conferir no Manual de Orientação do Contribuinte. Já barrou uma fonte errada que circula na web dando a série da chave com 6 dígitos e CT-e como modelo 67 — o modelo do CT-e é 57; 67 é CT-e OS.
Para adicionar uma corrupção: escreva a regra no REGRAS.md, some ao catálogo
da família em src/core/, e adicione o teste. A autoverificação faz o resto —
corrupção que devolve documento válido lança, em vez de virar teste que passa
sem ter testado nada.
Licença
MIT
