-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy patherrors_test.go
More file actions
262 lines (251 loc) · 8.93 KB
/
Copy patherrors_test.go
File metadata and controls
262 lines (251 loc) · 8.93 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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
package dpmaconnect
import (
"bytes"
"errors"
"io"
"net/http"
"strings"
"testing"
)
func TestXMLRootName(t *testing.T) {
tests := []struct {
name string
body []byte
want string
}{
{"transaction with declaration", []byte(`<?xml version="1.0" encoding="UTF-8"?><Transaction/>`), "Transaction"},
{"declaration-less error", []byte(`<Error Message_DE="x" Message_EN="y"/>`), "Error"},
{"patent hit list fixture", patentSearchXML, "PatentHitList"},
{"trademark hit list fixture", trademarkSearchXML, "HitList"},
{"design hit list fixture", designSearchXML, "DesignHitList"},
{"trademark error fixture", trademarkSearchErrorXML, "HitList"},
{"design error fixture", designSearchErrorXML, "DesignHitList"},
{"patent info fixture", patentInfoXML, "dpma-patent-document"},
{"BOM prefix", append([]byte{0xEF, 0xBB, 0xBF}, []byte(`<Transaction/>`)...), "Transaction"},
{"leading whitespace", []byte("\n <HitList/>"), "HitList"},
{"zip payload", []byte("PK\x03\x04binary"), ""},
{"pdf payload", []byte("%PDF-1.7 binary"), ""},
{"plain text", []byte("not xml at all"), ""},
{"empty", nil, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := xmlRootName(tt.body); got != tt.want {
t.Errorf("xmlRootName() = %q, want %q", got, tt.want)
}
})
}
}
// Binary bulk payloads must never be fed to xml.Unmarshal: only the bounded
// head is inspected, so a large ZIP body is cheap to classify and yields no
// false error.
func TestParseDPMAError_BinaryBodySkipsXMLParsing(t *testing.T) {
body := append([]byte("PK\x03\x04"), bytes.Repeat([]byte{0x42}, 1<<20)...)
if err := parseDPMAError(body, http.StatusOK); err != nil {
t.Errorf("parseDPMAError(zip, 200) = %v, want nil", err)
}
err := parseDPMAError(body, http.StatusInternalServerError)
var apiErr *APIError
if !errors.As(err, &apiErr) {
t.Fatalf("parseDPMAError(zip, 500) = %T, want *APIError fallback", err)
}
}
// The no-error-XML fallback must produce the typed taxonomy: AuthError for
// 401/403, RateLimitError for 429, generic APIError otherwise. Responses that
// do carry DPMA error XML keep their content-typed *APIError (E002 on 403).
func TestParseDPMAError_TypedFallbacks(t *testing.T) {
tests := []struct {
name string
body []byte
statusCode int
check func(t *testing.T, err error)
}{
{
"401 empty body", nil, http.StatusUnauthorized,
func(t *testing.T, err error) {
var authErr *AuthError
if !errors.As(err, &authErr) {
t.Fatalf("got %T (%v), want *AuthError", err, err)
}
if authErr.StatusCode != http.StatusUnauthorized {
t.Errorf("StatusCode = %d, want 401", authErr.StatusCode)
}
},
},
{
"403 html body", []byte("<html><body>Forbidden</body></html>"), http.StatusForbidden,
func(t *testing.T, err error) {
var authErr *AuthError
if !errors.As(err, &authErr) {
t.Fatalf("got %T (%v), want *AuthError", err, err)
}
if !strings.Contains(authErr.Message, "Forbidden") {
t.Errorf("Message = %q, want body preview", authErr.Message)
}
},
},
{
"429 plain body", []byte("too many requests"), http.StatusTooManyRequests,
func(t *testing.T, err error) {
var rlErr *RateLimitError
if !errors.As(err, &rlErr) {
t.Fatalf("got %T (%v), want *RateLimitError", err, err)
}
},
},
{
"500 stays APIError", []byte("boom"), http.StatusInternalServerError,
func(t *testing.T, err error) {
var apiErr *APIError
if !errors.As(err, &apiErr) {
t.Fatalf("got %T (%v), want *APIError", err, err)
}
},
},
{
"403 with E002 transaction XML stays APIError",
[]byte(`<?xml version="1.0"?><Transaction><PatentTransactionBody><TransactionErrorDetails><TransactionError><TransactionErrorCode>E002</TransactionErrorCode><TransactionErrorText>Permission denied</TransactionErrorText></TransactionError></TransactionErrorDetails></PatentTransactionBody></Transaction>`),
http.StatusForbidden,
func(t *testing.T, err error) {
var apiErr *APIError
if !errors.As(err, &apiErr) {
t.Fatalf("got %T (%v), want *APIError", err, err)
}
if apiErr.Code != "E002" {
t.Errorf("Code = %q, want E002", apiErr.Code)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := parseDPMAError(tt.body, tt.statusCode)
if err == nil {
t.Fatal("expected an error")
}
tt.check(t, err)
})
}
}
// The real search-error fixtures must surface as errors while their success
// twins pass through cleanly, with the unmarshal gated on the root element.
func TestParseDPMAError_FixtureEnvelopes(t *testing.T) {
tests := []struct {
name string
body []byte
wantMsg string // non-empty: an *APIError whose Message contains this
}{
{"trademark search error", trademarkSearchErrorXML, "not admissible"},
{"design search error", designSearchErrorXML, "not admissible"},
// A patent bad query arrives as <PatentHitList HitCount="0"
// Message_*=.../>, not the trademark service's <HitList><ErrorMessage>.
{"patent search error", patentSearchErrorXML, "not admissible"},
// Authentication failures arrive with HTTP 200 as an <Error> envelope;
// "Authentification" is DPMA's own spelling.
{"auth failure", authErrorXML, "Authentification failed"},
{"patent search success", patentSearchXML, ""},
{"patent search zero hits", patentSearchEmptyXML, ""},
{"trademark search success", trademarkSearchXML, ""},
{"design search success", designSearchXML, ""},
{"patent info success", patentInfoXML, ""},
// Capped result lists carry the truncation notice (patent/design:
// Message_* attributes on the root) but hold real hits; they must not
// classify as errors.
{"patent search capped", patentSearchLimitedXML, ""},
{"trademark search capped", trademarkSearchLimitedXML, ""},
{"design search capped", designSearchLimitedXML, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := parseDPMAError(tt.body, http.StatusOK)
if (err != nil) != (tt.wantMsg != "") {
t.Fatalf("parseDPMAError() error = %v, wantErr %v", err, tt.wantMsg != "")
}
if tt.wantMsg != "" {
var apiErr *APIError
if !errors.As(err, &apiErr) {
t.Fatalf("got %T, want *APIError", err)
}
if !strings.Contains(apiErr.Message, tt.wantMsg) {
t.Errorf("Message = %q, want it to contain %q", apiErr.Message, tt.wantMsg)
}
}
})
}
// The real "Data not available" Transaction envelope maps to the dedicated
// type, not a generic *APIError.
t.Run("data not available", func(t *testing.T) {
err := parseDPMAError(dataNotAvailableXML, http.StatusOK)
var dnaErr *DataNotAvailableError
if !errors.As(err, &dnaErr) {
t.Fatalf("got %T (%v), want *DataNotAvailableError", err, err)
}
})
}
// A declaration-less <Error .../> root (patent info endpoint) must be detected.
func TestParseDPMAError_DeclarationlessError(t *testing.T) {
body := []byte(`<Error Message_DE="Nicht gefunden" Message_EN="Not found"/>`)
err := parseDPMAError(body, http.StatusOK)
var apiErr *APIError
if !errors.As(err, &apiErr) {
t.Fatalf("got %T (%v), want *APIError", err, err)
}
if apiErr.Message != "Not found" {
t.Errorf("Message = %q, want Not found", apiErr.Message)
}
}
// streamResponse previously only recognized "<?xml"/"<Tra" prefixes, missing the
// declaration-less <Error .../> and hit-list envelopes: those were streamed into
// the destination file as if they were payload. All XML bodies are now inspected.
func TestStreamResponse_DetectsAllErrorEnvelopes(t *testing.T) {
tests := []struct {
name string
body []byte
}{
{"declaration-less error", []byte(`<Error Message_DE="x" Message_EN="kaputt"/>`)},
{"trademark hit list error", trademarkSearchErrorXML},
{"design hit list error", designSearchErrorXML},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(tt.body)),
}
var buf bytes.Buffer
err := streamResponse(resp, nil, "test", &buf)
var apiErr *APIError
if !errors.As(err, &apiErr) {
t.Fatalf("got %T (%v), want *APIError", err, err)
}
if buf.Len() != 0 {
t.Errorf("error body must not be written to the destination, got %d bytes", buf.Len())
}
})
}
}
// Binary PDF payloads and non-error XML must still stream through unchanged.
func TestStreamResponse_Passthrough(t *testing.T) {
tests := []struct {
name string
body []byte
}{
{"pdf binary", append([]byte("%PDF-1.7 "), bytes.Repeat([]byte{0x37}, 256)...)},
{"non-error hit list XML", patentSearchXML},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(tt.body)),
}
var buf bytes.Buffer
if err := streamResponse(resp, nil, "test", &buf); err != nil {
t.Fatalf("streamResponse() error = %v", err)
}
if !bytes.Equal(buf.Bytes(), tt.body) {
t.Errorf("streamed %d bytes, want %d unchanged", buf.Len(), len(tt.body))
}
})
}
}