-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjq_test.go
More file actions
336 lines (305 loc) · 10 KB
/
Copy pathjq_test.go
File metadata and controls
336 lines (305 loc) · 10 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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
package httpassert
import (
"context"
"errors"
"net/http"
"strings"
"testing"
"time"
"github.com/itchyny/gojq"
)
const jqDoc = `{"status":"success","count":5,"active":true,"nothing":null,
"users":[{"id":1,"name":"alice","active":true},{"id":2,"name":"bob","active":false}]}`
// jqResponse builds a response carrying the document above, or whatever body a
// case needs.
func jqResponse(body string) *Response {
r := response("200 OK", http.Header{}, "")
r.BodyBytes = []byte(body)
return &r
}
// Test_AssertJQ is the verdict table: what a query must yield for the assertion
// to hold, and what the failure says when it does not.
func Test_AssertJQ(t *testing.T) {
t.Parallel()
tests := []struct {
Name string
Query string
Body string
// Want is the expected error text, empty for a passing assertion.
Want string
}{
{Name: "a true comparison", Query: `.status == "success"`, Body: jqDoc},
{Name: "a numeric comparison", Query: `.count == 5`, Body: jqDoc},
{Name: "a boolean field read directly", Query: `.active`, Body: jqDoc},
{Name: "a builtin", Query: `.users | length == 2`, Body: jqDoc},
{Name: "a regexp, which jq has of its own", Query: `.status | test("^suc")`, Body: jqDoc},
{
// Several outputs, all true. Every one has to hold, or the
// assertion would pass on the strength of the first.
Name: "every output true",
Query: `.users[] | .id > 0`,
Body: jqDoc,
},
{
Name: "one output false among several",
Query: `.users[] | .active`,
Body: jqDoc,
Want: `jq[.users[] | .active]: expected true, got false`,
},
{
// The case that makes this more than a nicety: a query matching
// nothing has checked nothing, and a pass would be a green run
// with no check behind it.
Name: "no output at all",
Query: `.users[] | select(.id == 99) | .active`,
Body: jqDoc,
Want: `jq[.users[] | select(.id == 99) | .active]: expected true, got no output`,
},
{
Name: "a non-boolean output",
Query: `.status`,
Body: jqDoc,
Want: `jq[.status]: expected true, got "success"`,
},
{
// A missing key yields null rather than an error, so without this
// the failure would read as an unhelpful "got <nil>".
Name: "a missing key",
Query: `.nope`,
Body: jqDoc,
Want: `jq[.nope]: expected true, got null`,
},
{
Name: "a JSON null field",
Query: `.nothing`,
Body: jqDoc,
Want: `jq[.nothing]: expected true, got null`,
},
{
// Reported as a broken query, not as a failed assertion. The
// service answered correctly; the expression is what is wrong.
Name: "a runtime type error",
Query: `.status + 1`,
Body: jqDoc,
Want: `jq[.status + 1]: cannot add: string ("success") and number (1)`,
},
{
Name: "an object output",
Query: `.users[0]`,
Body: jqDoc,
Want: `jq[.users[0]]: expected true, got {"active":true,"id":1,"name":"alice"}`,
},
{
Name: "a body that is not JSON",
Query: `. == 1`,
Body: "plain text",
Want: "body: expected JSON, got invalid character 'p' looking for beginning of value",
},
{
// A 204 has no body to decode. The message names the body rather
// than blaming the expression.
Name: "an empty body",
Query: `. == 1`,
Body: "",
Want: "body: expected JSON, got unexpected end of JSON input",
},
{
// Valid JSON that is not an object. jq is happy; so is this.
Name: "a top-level array",
Query: `length == 3`,
Body: `[1,2,3]`,
},
{
Name: "a top-level scalar",
Query: `. == 42`,
Body: `42`,
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
a, err := AssertJQ(tc.Query)
if err != nil {
t.Fatalf("cannot build the assertion: %s", err)
}
checkErr(t, "jq", check(a, jqResponse(tc.Body)), tc.Want)
})
}
}
// Test_AssertJQ_rejectsBadQueries covers the two ways a query can be invalid.
// They are caught at different stages, and only catching one would let a typo
// reach the response and be reported as a failed assertion.
func Test_AssertJQ_rejectsBadQueries(t *testing.T) {
t.Parallel()
tests := []struct {
Name string
Query string
Want string
Why string
}{
{"unterminated", `.users[`, "unexpected EOF", "rejected while parsing"},
{"a dangling pipe", `.a |`, "unexpected EOF", "rejected while parsing"},
{
// Parses cleanly; only compiling notices. This is why both stages
// run before the request is made.
Name: "an undefined function", Query: `nope(.)`,
Want: "function not defined: nope/1", Why: "rejected while compiling",
},
{
Name: "an undefined variable", Query: `. as $x | $y`,
Want: "variable not defined: $y", Why: "rejected while compiling",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
a, err := AssertJQ(tc.Query)
if err == nil {
t.Fatalf("%q was accepted; expected it to be %s", tc.Query, tc.Why)
}
if a != nil {
t.Error("expected a nil Assertion alongside the error")
}
if got := err.Error(); !strings.Contains(got, tc.Want) {
t.Errorf("error = %q, want it to contain %q", got, tc.Want)
}
})
}
}
// Test_AssertJQ_boundsARunawayQuery: jq is a real language, so a query can
// simply never finish. Nothing else a caller can type makes this program hang
// -- the request is bounded by --max-time, the retry loop by --retry, and an
// --assert-body pattern cannot blow up because Go's regexp engine is
// linear-time. This is what keeps that true.
//
// Driven through runJQ with a short deadline rather than through the assertion
// with the real one: waiting out 10s twice would add 20s to a unit suite that
// otherwise runs in a quarter of a second, on every one of CI's six legs.
func Test_AssertJQ_boundsARunawayQuery(t *testing.T) {
t.Parallel()
const short = 50 * time.Millisecond
// Both compile cleanly. They are valid jq rather than malformed input, so
// no amount of validation at flag time could catch them -- which is why the
// bound has to exist at all.
for _, query := range []string{`def f: f; f`, `reduce range(1e9) as $i (0; .+$i)`} {
t.Run(query, func(t *testing.T) {
q, err := gojq.Parse(query)
if err != nil {
t.Fatalf("expected %q to parse: %s", query, err)
}
code, err := gojq.Compile(q)
if err != nil {
t.Fatalf("expected %q to compile: %s", query, err)
}
done := make(chan error, 1)
start := time.Now()
go func() {
// Either return means the query did not succeed; the test
// only cares that it stopped, and why is asserted below.
f, err := runJQ(code, query, jqResponse(jqDoc), short)
if err == nil && f != nil {
err = textError("query returned an assertion failure")
}
done <- err
}()
select {
case err := <-done:
if err == nil {
t.Fatal("a non-terminating query reported success")
}
// Stopping early would mean it never really ran, so the
// deadline is asserted from both sides.
if d := time.Since(start); d < short {
t.Errorf("stopped after %v, before the %v deadline: %s", d, short, err)
}
case <-time.After(30 * time.Second):
t.Fatal("still running long past the deadline")
}
})
}
}
func Test_AssertJQ_honorsRequestCancellation(t *testing.T) {
t.Parallel()
assertion, err := AssertJQ(`def f: f; f`)
if err != nil {
t.Fatalf("AssertJQ: %s", err)
}
ctx, cancel := context.WithCancel(context.Background())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.test/health", nil)
if err != nil {
t.Fatalf("NewRequestWithContext: %s", err)
}
res := jqResponse(jqDoc)
res.Request = req
cancel()
done := make(chan error, 1)
go func() {
failure, checkErr := assertion.Check(res)
if failure != nil {
checkErr = errors.New("cancelled query returned an assertion failure")
}
done <- checkErr
}()
select {
case checkErr := <-done:
if !errors.Is(checkErr, context.Canceled) {
t.Fatalf("error = %v, want context.Canceled", checkErr)
}
var evaluation *EvaluationError
if !errors.As(checkErr, &evaluation) || evaluation.Code != EvaluationJQ {
t.Errorf("error = %T %+v, want jq EvaluationError", checkErr, evaluation)
}
case <-time.After(time.Second):
t.Fatal("jq evaluation ignored the cancelled request context")
}
}
// Test_jqTimeout pins the deadline the assertion actually uses. The test above
// injects its own, so without this the real value could drift to something
// useless and nothing would notice.
func Test_jqTimeout(t *testing.T) {
t.Parallel()
if got, want := jqTimeout, 10*time.Second; got != want {
t.Errorf("jqTimeout = %v, want %v", got, want)
}
}
// Test_decodeJSON_parsesOnce pins the sharing. Ten --assert-jq flags read one
// response, and it should be decoded once rather than ten times.
func Test_decodeJSON_parsesOnce(t *testing.T) {
t.Parallel()
res := jqResponse(jqDoc)
first, err := res.decodeJSON()
if err != nil {
t.Fatalf("first decode: %s", err)
}
second, err := res.decodeJSON()
if err != nil {
t.Fatalf("second decode: %s", err)
}
// Same map, not an equal one: a second Unmarshal would produce a distinct
// value, so identity is what proves the body was parsed once.
m1, ok1 := first.(map[string]any)
m2, ok2 := second.(map[string]any)
if !ok1 || !ok2 {
t.Fatalf("expected a JSON object, got %T and %T", first, second)
}
m1["probe"] = true
if _, carried := m2["probe"]; !carried {
t.Error("decodeJSON re-parsed the body instead of sharing the first result")
}
t.Run("a failure is remembered too", func(t *testing.T) {
bad := jqResponse("not json")
if _, err := bad.decodeJSON(); err == nil {
t.Fatal("expected an error")
}
if _, err := bad.decodeJSON(); err == nil {
t.Error("the second call lost the error the first recorded")
}
})
// A body that is still compressed must report that, not invalid JSON --
// the reader would otherwise go looking for a malformed payload (#27).
t.Run("an undecoded body reports the encoding", func(t *testing.T) {
enc := jqResponse(jqDoc)
enc.Encoding = "compress"
enc.DecodeErr = errors.New("no decoder for \"compress\"")
_, err := enc.decodeJSON()
checkErrMatch(t, "decodeJSON", err, `^body: response is compress-encoded`)
})
}