-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathresponse_json.go
More file actions
342 lines (317 loc) · 9.8 KB
/
Copy pathresponse_json.go
File metadata and controls
342 lines (317 loc) · 9.8 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
337
338
339
340
341
342
package zentests
import (
"encoding/json"
"regexp"
"strconv"
"strings"
"github.com/stretchr/testify/assert"
)
// JSON parses and caches the response body as JSON.
// Enables JSON-specific assertions on the response. The parsed JSON is cached
// to avoid re-parsing on subsequent JSON assertions. Fails the test if JSON
// parsing fails.
//
// Returns:
// - *Response: The receiver for method chaining
//
// Example:
//
// zt.Get(app, "/api/users").JSON().Has("data.0.name", "John")
func (r *Response) JSON() *Response {
if r.parsedJSON == nil {
err := json.Unmarshal(r.Body(), &r.parsedJSON)
assert.NoError(r.t, err, "JSON parsing failed")
}
return r
}
// HasKey asserts that the JSON response contains the specified key path.
// Supports dot notation for nested keys and array indices (e.g., "users.0.name").
//
// Parameters:
// - path: The JSON key path using dot notation (e.g., "user.email", "items.0.id")
//
// Returns:
// - *Response: The receiver for method chaining
//
// Example:
//
// zt.Get(app, "/api/users/1").HasKey("data.user.name")
func (r *Response) HasKey(path string) *Response {
r.JSON()
_, exists := getNestedValue(r.parsedJSON, path)
assert.True(r.t, exists, "JSON should have key %q", path)
return r
}
// Has asserts that the JSON key equals the expected value with strict type checking.
// Supports dot notation for nested keys. Fails if the key doesn't exist or if
// the type doesn't match exactly.
//
// Parameters:
// - path: The JSON key path using dot notation
// - expected: The expected value (type must match exactly)
//
// Returns:
// - *Response: The receiver for method chaining
//
// Example:
//
// zt.Get(app, "/api/users/1").Has("data.user.name", "John")
// zt.Get(app, "/api/count").Has("data.count", float64(42)) // JSON numbers are float64
func (r *Response) Has(path string, expected any) *Response {
r.JSON()
actual, exists := getNestedValue(r.parsedJSON, path)
assert.True(r.t, exists, "JSON key %q not found", path)
// Type strictness: compare types first
assert.IsType(r.t, expected, actual, "type mismatch for key %q", path)
assert.Equal(r.t, expected, actual, "value mismatch for key %q", path)
return r
}
// HasInt asserts that the JSON key equals the expected integer value.
// Handles JSON number conversion (JSON numbers are parsed as float64).
//
// Parameters:
// - path: The JSON key path using dot notation
// - expected: The expected integer value
//
// Returns:
// - *Response: The receiver for method chaining
//
// Example:
//
// zt.Get(app, "/api/count").HasInt("data.total", 42)
func (r *Response) HasInt(path string, expected int) *Response {
r.JSON()
actual, exists := getNestedValue(r.parsedJSON, path)
assert.True(r.t, exists, "JSON key %q not found", path)
// JSON numbers are float64, convert for comparison
switch v := actual.(type) {
case float64:
assert.Equal(r.t, expected, int(v), "int value mismatch for key %q", path)
case int:
assert.Equal(r.t, expected, v, "int value mismatch for key %q", path)
default:
assert.Fail(r.t, "type mismatch", "expected numeric value for key %q, got %T", path, actual)
}
return r
}
// HasFloat asserts that the JSON key equals the expected float64 value.
// Uses InDelta for comparison to handle floating point precision issues.
//
// Parameters:
// - path: The JSON key path using dot notation
// - expected: The expected float64 value
//
// Returns:
// - *Response: The receiver for method chaining
//
// Example:
//
// zt.Get(app, "/api/score").HasFloat("data.score", 95.5)
func (r *Response) HasFloat(path string, expected float64) *Response {
r.JSON()
actual, exists := getNestedValue(r.parsedJSON, path)
assert.True(r.t, exists, "JSON key %q not found", path)
// Handle both float64 and int from JSON
switch v := actual.(type) {
case float64:
assert.InDelta(r.t, expected, v, 0.0001, "float value mismatch for key %q", path)
case int:
assert.InDelta(r.t, expected, float64(v), 0.0001, "float value mismatch for key %q", path)
default:
assert.Fail(r.t, "type mismatch", "expected numeric value for key %q, got %T", path, actual)
}
return r
}
// HasString asserts that the JSON key equals the expected string value.
//
// Parameters:
// - path: The JSON key path using dot notation
// - expected: The expected string value
//
// Returns:
// - *Response: The receiver for method chaining
//
// Example:
//
// zt.Get(app, "/api/users/1").HasString("data.user.email", "john@example.com")
func (r *Response) HasString(path, expected string) *Response {
r.JSON()
actual, exists := getNestedValue(r.parsedJSON, path)
assert.True(r.t, exists, "JSON key %q not found", path)
assert.Equal(r.t, expected, actual, "string value mismatch for key %q", path)
return r
}
// HasBool asserts that the JSON key equals the expected boolean value.
//
// Parameters:
// - path: The JSON key path using dot notation
// - expected: The expected boolean value
//
// Returns:
// - *Response: The receiver for method chaining
//
// Example:
//
// zt.Get(app, "/api/status").HasBool("data.active", true)
func (r *Response) HasBool(path string, expected bool) *Response {
r.JSON()
actual, exists := getNestedValue(r.parsedJSON, path)
assert.True(r.t, exists, "JSON key %q not found", path)
assert.Equal(r.t, expected, actual, "bool value mismatch for key %q", path)
return r
}
// MatchesRegex asserts that the JSON key value matches the regex pattern.
// The value at the specified path must be a string.
//
// Parameters:
// - path: The JSON key path using dot notation
// - pattern: The regex pattern to match
//
// Returns:
// - *Response: The receiver for method chaining
//
// Example:
//
// zt.Get(app, "/api/users/1").MatchesRegex("data.user.email", `^[\w.-]+@[\w.-]+\.\w+$`)
func (r *Response) MatchesRegex(path, pattern string) *Response {
r.JSON()
actual, exists := getNestedValue(r.parsedJSON, path)
assert.True(r.t, exists, "JSON key %q not found", path)
re, err := regexp.Compile(pattern)
if err != nil {
assert.Fail(r.t, "invalid regex pattern: %v", err)
return r
}
str, ok := actual.(string)
assert.True(r.t, ok, "expected string value for key %q to match regex, got %T", path, actual)
matched := re.MatchString(str)
assert.True(r.t, matched, "value %q should match pattern %q for key %s", str, pattern, path)
return r
}
// JSONMatches asserts that the entire JSON structure matches the expected map.
// Performs Has() assertions for each key-value pair in the expected map.
//
// Parameters:
// - expected: Map of key paths to expected values
//
// Returns:
// - *Response: The receiver for method chaining
//
// Example:
//
// zt.Get(app, "/api/users/1").JSONMatches(map[string]interface{}{
// "data.user.name": "John",
// "data.user.active": true,
// })
func (r *Response) JSONMatches(expected map[string]any) *Response {
r.JSON()
for path, expectedValue := range expected {
r.Has(path, expectedValue)
}
return r
}
// ArrayLength asserts that the JSON key is an array with the expected length.
//
// Parameters:
// - path: The JSON key path using dot notation
// - expected: The expected array length
//
// Returns:
// - *Response: The receiver for method chaining
//
// Example:
//
// zt.Get(app, "/api/users").ArrayLength("data.users", 10)
func (r *Response) ArrayLength(path string, expected int) *Response {
r.JSON()
actual, exists := getNestedValue(r.parsedJSON, path)
assert.True(r.t, exists, "JSON key %q not found", path)
arr, ok := actual.([]any)
assert.True(r.t, ok, "expected array for key %q, got %T", path, actual)
assert.Equal(r.t, expected, len(arr), "array length mismatch for key %q", path)
return r
}
// IsNull asserts that the JSON key has a null value.
//
// Parameters:
// - path: The JSON key path using dot notation
//
// Returns:
// - *Response: The receiver for method chaining
//
// Example:
//
// zt.Get(app, "/api/users/999").IsNull("data.user")
func (r *Response) IsNull(path string) *Response {
r.JSON()
actual, exists := getNestedValue(r.parsedJSON, path)
assert.True(r.t, exists, "JSON key %q not found", path)
assert.Nil(r.t, actual, "expected null for key %q", path)
return r
}
// IsNotNull asserts that the JSON key has a non-null value.
//
// Parameters:
// - path: The JSON key path using dot notation
//
// Returns:
// - *Response: The receiver for method chaining
//
// Example:
//
// zt.Get(app, "/api/users/1").IsNotNull("data.user")
func (r *Response) IsNotNull(path string) *Response {
r.JSON()
actual, exists := getNestedValue(r.parsedJSON, path)
assert.True(r.t, exists, "JSON key %q not found", path)
assert.NotNil(r.t, actual, "expected non-null for key %q", path)
return r
}
// getNestedValue traverses a dot-notation path through JSON data.
// Internal helper function that supports nested objects and array indices.
// Returns the value and a boolean indicating if the path exists.
//
// Parameters:
// - data: The root JSON object as a map
// - path: Dot-notation path (e.g., "user.name", "items.0.name")
//
// Returns:
// - interface{}: The value at the path (nil if not found)
// - bool: True if the path exists, false otherwise
//
// Example:
//
// // Internal usage - supports paths like:
// // "user.name" -> accesses data["user"]["name"]
// // "items.0.id" -> accesses data["items"][0]["id"]
func getNestedValue(data map[string]any, path string) (any, bool) {
parts := strings.Split(path, ".")
current := any(data)
for _, part := range parts {
if current == nil {
return nil, false
}
switch v := current.(type) {
case map[string]any:
val, exists := v[part]
if !exists {
return nil, false
}
current = val
case []any:
// Parse array index
index, err := strconv.Atoi(part)
if err != nil {
return nil, false // not a valid index
}
if index < 0 || index >= len(v) {
return nil, false // out of bounds
}
current = v[index]
default:
// Can't traverse further
return nil, false
}
}
return current, true
}