-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathubl_test.go
More file actions
193 lines (152 loc) · 6.56 KB
/
Copy pathubl_test.go
File metadata and controls
193 lines (152 loc) · 6.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
package ubl_test
import (
"errors"
"strings"
"testing"
"github.com/invopop/gobl"
"github.com/invopop/gobl.fr.ctc/addon/flow2"
ubl "github.com/invopop/gobl.ubl"
"github.com/invopop/gobl/addons/eu/en16931"
"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/note"
"github.com/invopop/gobl/rules"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParse(t *testing.T) {
t.Run("invoice namespace returns *Invoice", func(t *testing.T) {
data, err := testLoadXML("en16931/ubl-example1.xml")
require.NoError(t, err)
doc, err := ubl.Parse(data)
require.NoError(t, err)
inv, ok := doc.(*ubl.Invoice)
require.True(t, ok, "expected *ubl.Invoice")
assert.Equal(t, "urn:cen.eu:en16931:2017", inv.CustomizationID)
})
t.Run("credit note namespace returns *Invoice", func(t *testing.T) {
data, err := testLoadXML("en16931/credit-note1.xml")
require.NoError(t, err)
doc, err := ubl.Parse(data)
require.NoError(t, err)
_, ok := doc.(*ubl.Invoice)
require.True(t, ok, "expected *ubl.Invoice for CreditNote documents")
})
t.Run("unknown root namespace returns ErrUnknownDocumentType", func(t *testing.T) {
data := []byte(`<?xml version="1.0" encoding="UTF-8"?>
<Foo xmlns="urn:example:foo"><Bar/></Foo>`)
_, err := ubl.Parse(data)
assert.ErrorIs(t, err, ubl.ErrUnknownDocumentType)
})
t.Run("empty input returns ErrUnknownDocumentType", func(t *testing.T) {
_, err := ubl.Parse(nil)
assert.ErrorIs(t, err, ubl.ErrUnknownDocumentType)
})
t.Run("malformed XML returns a parse error", func(t *testing.T) {
_, err := ubl.Parse([]byte("<not-closed"))
require.Error(t, err)
assert.False(t, errors.Is(err, ubl.ErrUnknownDocumentType))
assert.Contains(t, err.Error(), "error parsing XML")
})
}
func TestConvertDefaultContext(t *testing.T) {
// Calling Convert without WithContext should fall back to EN16931.
env := loadTestEnvelope(t, "invoice-minimal.json")
doc, err := ubl.Convert(env)
require.NoError(t, err)
inv, ok := doc.(*ubl.Invoice)
require.True(t, ok)
assert.Equal(t, ubl.ContextEN16931.CustomizationID, inv.CustomizationID)
assert.Empty(t, inv.ProfileID)
}
func TestConvertAutomaticallyAddsRequiredAddons(t *testing.T) {
t.Run("injects missing addon from context", func(t *testing.T) {
// Load a France CTC-shaped invoice, strip the ctc addon, and verify
// that Convert injects it back in before producing the UBL document.
env := loadTestEnvelope(t, "france-cius/invoice-fr-cius.json")
inv, ok := env.Extract().(*bill.Invoice)
require.True(t, ok)
// Drop the ctc addon; keep the en16931 one. SetAddons replaces the list.
inv.SetAddons(en16931.V2017)
require.NotContains(t, inv.GetAddons(), flow2.V1,
"precondition: ctc addon must be absent before Convert runs")
_, err := ubl.Convert(env, ubl.WithContext(ubl.ContextPeppolFranceCIUS))
require.NoError(t, err)
// After Convert the addon should have been appended in-place.
assert.Contains(t, inv.GetAddons(), flow2.V1)
// And the pre-existing addon must be preserved.
assert.Contains(t, inv.GetAddons(), en16931.V2017)
})
t.Run("no-op when addon is already present", func(t *testing.T) {
env := loadTestEnvelope(t, "invoice-minimal.json")
inv, ok := env.Extract().(*bill.Invoice)
require.True(t, ok)
before := append([]cbc.Key(nil), inv.GetAddons()...)
require.Contains(t, before, en16931.V2017)
_, err := ubl.Convert(env, ubl.WithContext(ubl.ContextEN16931))
require.NoError(t, err)
assert.Equal(t, before, inv.GetAddons(),
"addon list should be unchanged when all required addons are already set")
})
}
func TestConvertSurfacesValidationFaultsAfterAutoAddon(t *testing.T) {
// When Convert auto-injects a stricter addon, the resulting validation
// failure must be surfaced as a *gobl.Error whose cause is rules.Faults,
// so consumers can render the []*rules.Fault list (code, paths, message)
// instead of a flattened string.
// Minimal DE invoice doesn't satisfy the France CTC rule set. Convert
// with the France CIUS context to force ensureAddons to add flow2.V1
// and then fail validation.
env := loadTestEnvelope(t, "invoice-minimal.json")
_, err := ubl.Convert(env, ubl.WithContext(ubl.ContextPeppolFranceCIUS))
require.Error(t, err)
// Must be the GOBL validation error — not wrapped in anything ubl-specific.
assert.ErrorIs(t, err, gobl.ErrValidation)
var ge *gobl.Error
require.ErrorAs(t, err, &ge, "error must be a *gobl.Error so faults survive")
faults := ge.Faults()
require.NotNil(t, faults, "cause must be rules.Faults, not a plain error")
require.Greater(t, faults.Len(), 0)
// Faults().List() returns []*rules.Fault — each fault keeps its
// structured code, paths, and message so it can be rendered by a client.
list := faults.List()
assert.IsType(t, []*rules.Fault{}, list)
require.NotEmpty(t, list)
first := list[0]
assert.NotEmpty(t, first.Code(), "fault must carry a rule code")
assert.NotEmpty(t, first.Message(), "fault must carry a message")
assert.NotEmpty(t, first.Paths(), "fault must carry at least one JSON path")
// The France CTC addon's "supplier inboxes are required for French B2B
// invoices" rule must be among the reported faults. (The billing-mode rule
// is now auto-satisfied by the addon's normalization, so a structural rule
// the minimal invoice cannot satisfy is used instead.)
assert.True(t, faults.HasCode("GOBL-FR-CTC-FLOW2-BILL-INVOICE-11"),
"expected supplier-inboxes-required fault; got: %s", err)
}
func TestConvertUnsupportedDocumentType(t *testing.T) {
// Build an envelope around a non-invoice document. Use a context with no
// required addons so ensureAddons exits early and we reach the type switch.
env, err := gobl.Envelop(¬e.Message{Content: "hello"})
require.NoError(t, err)
_, err = ubl.Convert(env, ubl.WithContext(ubl.Context{}))
assert.ErrorIs(t, err, ubl.ErrUnsupportedDocumentType)
}
func TestConvertRejectsUnsupportedDocument(t *testing.T) {
// A document type with no UBL mapping is rejected as unsupported.
env, err := gobl.Envelop(¬e.Message{Content: "hello"})
require.NoError(t, err)
_, err = ubl.Convert(env, ubl.WithContext(ubl.ContextEN16931))
require.Error(t, err)
assert.ErrorIs(t, err, ubl.ErrUnsupportedDocumentType)
}
func TestBytes(t *testing.T) {
env := loadTestEnvelope(t, "invoice-minimal.json")
doc, err := ubl.ConvertInvoice(env)
require.NoError(t, err)
out, err := ubl.Bytes(doc)
require.NoError(t, err)
s := string(out)
assert.True(t, strings.HasPrefix(s, `<?xml version="1.0" encoding="UTF-8"?>`),
"output should start with the standard XML header")
assert.Contains(t, s, "<Invoice")
}