-
Notifications
You must be signed in to change notification settings - Fork 2
/
api_testcase.go
579 lines (490 loc) · 17.4 KB
/
api_testcase.go
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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
package main
import (
"bufio"
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"strings"
"time"
"github.com/programmfabrik/apitest/pkg/lib/datastore"
"github.com/programmfabrik/golib"
"github.com/programmfabrik/apitest/pkg/lib/api"
"github.com/programmfabrik/apitest/pkg/lib/compare"
"github.com/programmfabrik/apitest/pkg/lib/report"
"github.com/programmfabrik/apitest/pkg/lib/template"
"github.com/programmfabrik/apitest/pkg/lib/util"
"github.com/sirupsen/logrus"
)
// Case defines the structure of our single testcase
// It gets read in by our config reader at the moment the mainfest.json gets parsed
type Case struct {
Name string `json:"name"`
Description string `json:"description"`
RequestData *any `json:"request"`
ResponseData any `json:"response"`
ContinueOnFailure bool `json:"continue_on_failure"`
Store map[string]any `json:"store"` // init datastore before testrun
StoreResponse map[string]string `json:"store_response_gjson"` // store gjson parsed response in datastore
Timeout int `json:"timeout_ms"`
WaitBefore *int `json:"wait_before_ms"`
WaitAfter *int `json:"wait_after_ms"`
Delay *int `json:"delay_ms"`
BreakResponse []any `json:"break_response"`
CollectResponse any `json:"collect_response"`
LogNetwork *bool `json:"log_network"`
LogVerbose *bool `json:"log_verbose"`
LogShort *bool `json:"log_short"`
loader template.Loader
manifestDir string
ReportElem *report.ReportElement
suiteIndex int
index int
dataStore *datastore.Datastore
standardHeader map[string]any // can be string or []string
standardHeaderFromStore map[string]string
ServerURL string `json:"server_url"`
ReverseTestResult bool `json:"reverse_test_result"`
Filename string
}
func (testCase Case) runAPITestCase(parentReportElem *report.ReportElement) (success bool) {
if testCase.Name == "" {
testCase.Name = "<no name>"
}
if testCase.LogShort == nil || !*testCase.LogShort {
if testCase.Description == "" {
logrus.Infof(" [%2d] '%s'", testCase.index, testCase.Name)
} else {
logrus.Infof(" [%2d] '%s': '%s'", testCase.index, testCase.Name, testCase.Description)
}
}
testCase.ReportElem = parentReportElem.NewChild(testCase.Name)
r := testCase.ReportElem
start := time.Now()
// Store standard data into datastore
if testCase.dataStore == nil && len(testCase.Store) > 0 {
err := fmt.Errorf("error setting datastore. Datastore is nil")
r.SaveToReportLog(fmt.Sprintf("Error during execution: %s", err))
logrus.Errorf(" [%2d] %s", testCase.index, err)
return false
}
err := testCase.dataStore.SetMap(testCase.Store)
if err != nil {
err = fmt.Errorf("error setting datastore map:%s", err)
r.SaveToReportLog(fmt.Sprintf("Error during execution: %s", err))
logrus.Errorf(" [%2d] %s", testCase.index, err)
return false
}
success = true
var apiResponse api.Response
if testCase.RequestData != nil {
success, apiResponse, err = testCase.run()
}
elapsed := time.Since(start)
if err != nil {
r.SaveToReportLog(fmt.Sprintf("Error during execution: %s", err))
if !testCase.ReverseTestResult || testCase.LogShort == nil || !*testCase.LogShort {
logrus.Errorf(" [%2d] %s", testCase.index, err)
}
success = false
}
// Reverse if needed
if testCase.ReverseTestResult {
success = !success
}
fileBasename := filepath.Base(testCase.Filename)
logF := logrus.Fields{
"elapsed": elapsed.String(),
"size": golib.HumanByteSize(uint64(len(apiResponse.Body))),
"request": apiResponse.ReqDur.String(),
"body": apiResponse.BodyLoadDur.String(),
"file": fileBasename,
}
if !success {
logrus.WithFields(logF).Warnf(" [%2d] failure", testCase.index)
} else if testCase.LogShort == nil || !*testCase.LogShort {
logrus.WithFields(logF).Infof(" [%2d] success", testCase.index)
}
r.Leave(success)
return success
// res.Success = success
// res.BodySize = uint64(len(apiResponse.Body))
// res.BodyLoadDur = apiResponse.BodyLoadDur
// res.RequestDur = apiResponse.ReqDur
// return res
}
// cheRckForBreak Response tests the given response for a so called break response.
// If this break response is present it returns a true
func (testCase Case) breakResponseIsPresent(response api.Response) (bool, error) {
if testCase.BreakResponse != nil {
for _, v := range testCase.BreakResponse {
spec, err := testCase.loadResponseSerialization(v)
if err != nil {
return false, fmt.Errorf("error loading check response serilization: %s", err)
}
expectedResponse, err := api.NewResponseFromSpec(spec)
if err != nil {
return false, fmt.Errorf("error loading check response from spec: %s", err)
}
if expectedResponse.Format.Type != "" {
response.Format = expectedResponse.Format
} else {
expectedResponse.Format = response.Format
}
responsesMatch, err := testCase.responsesEqual(expectedResponse, response)
if err != nil {
return false, fmt.Errorf("error matching break responses: %s", err)
}
if testCase.LogVerbose != nil && *testCase.LogVerbose {
logrus.Tracef("breakResponseIsPresent: %v", responsesMatch)
}
if responsesMatch.Equal {
return true, nil
}
}
}
return false, nil
}
// checkCollectResponse loops over all given collect responses and then
// If this continue response is present it returns a true.
// If no continue response is set, it also returns true to keep the testsuite running
func (testCase *Case) checkCollectResponse(response api.Response) (int, error) {
if testCase.CollectResponse != nil {
_, loadedResponses, err := template.LoadManifestDataAsObject(testCase.CollectResponse, testCase.manifestDir, testCase.loader)
if err != nil {
return -1, fmt.Errorf("error loading check response: %s", err)
}
var jsonRespArray util.JsonArray
switch t := loadedResponses.(type) {
case util.JsonArray:
jsonRespArray = t
case util.JsonObject:
jsonRespArray = util.JsonArray{t}
default:
return -1, fmt.Errorf("error loading check response no valid typew")
}
leftResponses := make(util.JsonArray, 0)
for _, v := range jsonRespArray {
spec, err := testCase.loadResponseSerialization(v)
if err != nil {
return -1, fmt.Errorf("error loading check response serilization: %s", err)
}
expectedResponse, err := api.NewResponseFromSpec(spec)
if err != nil {
return -1, fmt.Errorf("error loading check response from spec: %s", err)
}
if expectedResponse.Format.Type != "" || expectedResponse.Format.PreProcess != nil {
response.Format = expectedResponse.Format
} else {
expectedResponse.Format = response.Format
}
responsesMatch, err := testCase.responsesEqual(expectedResponse, response)
if err != nil {
return -1, fmt.Errorf("error matching check responses: %s", err)
}
// !eq && !reverse -> add
// !eq && reverse -> don't add
// eq && !reverse -> don't add
// eq && reverse -> add
if !responsesMatch.Equal && !testCase.ReverseTestResult ||
responsesMatch.Equal && testCase.ReverseTestResult {
leftResponses = append(leftResponses, v)
}
}
testCase.CollectResponse = leftResponses
if testCase.LogVerbose != nil && *testCase.LogVerbose {
logrus.Tracef("Remaining CheckReponses: %s", testCase.CollectResponse)
}
return len(leftResponses), nil
}
return 0, nil
}
func (testCase Case) executeRequest(counter int) (responsesMatch compare.CompareResult, req api.Request, apiResp api.Response, err error) {
// Store datastore
err = testCase.dataStore.SetMap(testCase.Store)
if err != nil {
err = fmt.Errorf("error setting datastore map:%s", err)
return responsesMatch, req, apiResp, err
}
//Do Request
req, err = testCase.loadRequest()
if err != nil {
err = fmt.Errorf("error loading request: %s", err)
return responsesMatch, req, apiResp, err
}
//Log request on trace level (so only v2 will trigger this)
if testCase.LogNetwork != nil && *testCase.LogNetwork {
logrus.Tracef("[REQUEST]:\n%s\n\n", limitLines(req.ToString(logCurl), Config.Apitest.Limit.Request))
}
expRes, err := testCase.loadExpectedResponse()
if err != nil {
testCase.LogReq(req)
err = fmt.Errorf("error loading response: %s", err)
return responsesMatch, req, apiResp, err
}
apiResp, err = req.Send()
if err != nil {
testCase.LogReq(req)
err = fmt.Errorf("error sending request: %s", err)
return responsesMatch, req, apiResp, err
}
apiResp.Format = expRes.Format
apiRespJsonString, err := apiResp.ServerResponseToJsonString(false)
// If we don't define an expected response, we won't have a format
// That's problematic if the response is not JSON, as we try to parse it for the datastore anyway
// So we don't fail the test in that edge case
if err != nil && (testCase.ResponseData != nil || len(testCase.StoreResponse) > 0) {
testCase.LogReq(req)
err = fmt.Errorf("error getting json from response: %s", err)
return responsesMatch, req, apiResp, err
}
// Store in custom store
err = testCase.dataStore.SetWithGjson(apiRespJsonString, testCase.StoreResponse)
if err != nil {
testCase.LogReq(req)
err = fmt.Errorf("error store response with gjson: %s", err)
return responsesMatch, req, apiResp, err
}
// Store in datastore -1 list
if counter == 0 {
testCase.dataStore.AppendResponse(apiRespJsonString)
} else {
testCase.dataStore.UpdateLastResponse(apiRespJsonString)
}
// Compare Responses
responsesMatch, err = testCase.responsesEqual(expRes, apiResp)
if err != nil {
testCase.LogReq(req)
err = fmt.Errorf("error matching responses: %s", err)
return responsesMatch, req, apiResp, err
}
return responsesMatch, req, apiResp, nil
}
// LogResp print the response to the console
func (testCase Case) LogResp(response api.Response) {
errString := fmt.Sprintf("[RESPONSE]:\n%s\n\n", limitLines(response.ToString(), Config.Apitest.Limit.Response))
if !testCase.ReverseTestResult && testCase.LogNetwork != nil && !*testCase.LogNetwork && !testCase.ContinueOnFailure {
testCase.ReportElem.SaveToReportLogF(errString)
logrus.Debug(errString)
}
}
// LogReq print the request to the console
func (testCase Case) LogReq(req api.Request) {
errString := fmt.Sprintf("[REQUEST]:\n%s\n\n", limitLines(req.ToString(logCurl), Config.Apitest.Limit.Request))
if !testCase.ReverseTestResult && !testCase.ContinueOnFailure && testCase.LogNetwork != nil && !*testCase.LogNetwork {
testCase.ReportElem.SaveToReportLogF(errString)
logrus.Debug(errString)
}
}
func limitLines(in string, limitCount int) string {
if limitCount <= 0 {
return in
}
out := ""
scanner := bufio.NewScanner(strings.NewReader(in))
k := 0
for scanner.Scan() && k < limitCount {
out += scanner.Text() + "\n"
k++
}
if k >= limitCount {
out += fmt.Sprintf("[Limited after %d lines]", limitCount)
}
return out
}
func (testCase Case) run() (successs bool, apiResponse api.Response, err error) {
var (
responsesMatch compare.CompareResult
request api.Request
timedOutFlag bool
)
startTime := time.Now()
r := testCase.ReportElem
requestCounter := 0
collectPresent := testCase.CollectResponse != nil
if testCase.WaitBefore != nil {
if testCase.LogShort == nil || !*testCase.LogShort {
logrus.Infof("wait_before_ms: %d", *testCase.WaitBefore)
}
time.Sleep(time.Duration(*testCase.WaitBefore) * time.Millisecond)
}
//Poll repeats the request until the right response is found, or a timeout triggers
for {
// delay between repeating a request
if testCase.Delay != nil {
time.Sleep(time.Duration(*testCase.Delay) * time.Millisecond)
}
responsesMatch, request, apiResponse, err = testCase.executeRequest(requestCounter)
if testCase.LogNetwork != nil && *testCase.LogNetwork {
logrus.Debugf("[RESPONSE]:\n%s\n\n", limitLines(apiResponse.ToString(), Config.Apitest.Limit.Response))
}
if err != nil {
testCase.LogResp(apiResponse)
return false, apiResponse, err
}
if responsesMatch.Equal && !collectPresent {
break
}
breakPresent, err := testCase.breakResponseIsPresent(apiResponse)
if err != nil {
testCase.LogReq(request)
testCase.LogResp(apiResponse)
return false, apiResponse, fmt.Errorf("error checking for break response: %s", err)
}
if breakPresent {
testCase.LogReq(request)
testCase.LogResp(apiResponse)
return false, apiResponse, fmt.Errorf("Break response found")
}
collectLeft, err := testCase.checkCollectResponse(apiResponse)
if err != nil {
testCase.LogReq(request)
testCase.LogResp(apiResponse)
return false, apiResponse, fmt.Errorf("error checking for continue response: %s", err)
}
if collectPresent && collectLeft <= 0 {
break
}
//break if timeout or we do not have a repeater
if timedOut := time.Since(startTime) > (time.Duration(testCase.Timeout) * time.Millisecond); timedOut && testCase.Timeout != -1 {
if timedOut && testCase.Timeout > 0 {
logrus.Warnf("Pull Timeout '%dms' exceeded", testCase.Timeout)
r.SaveToReportLogF("Pull Timeout '%dms' exceeded", testCase.Timeout)
timedOutFlag = true
}
break
}
requestCounter++
}
if !responsesMatch.Equal || timedOutFlag {
if !testCase.ReverseTestResult {
for _, v := range responsesMatch.Failures {
logrus.Errorf("[%s] %s", v.Key, v.Message)
r.SaveToReportLog(fmt.Sprintf("[%s] %s", v.Key, v.Message))
}
} else {
for _, v := range responsesMatch.Failures {
logrus.Infof("Reverse Test Result of: [%s] %s", v.Key, v.Message)
r.SaveToReportLog(fmt.Sprintf("reverse test result: [%s] %s", v.Key, v.Message))
}
}
collectArray, ok := testCase.CollectResponse.(util.JsonArray)
if ok {
for _, v := range collectArray {
jsonV, err := json.Marshal(v)
if err != nil {
testCase.LogReq(request)
testCase.LogResp(apiResponse)
return false, apiResponse, err
}
logrus.Errorf("Collect response not found: %s", jsonV)
r.SaveToReportLog(fmt.Sprintf("Collect response not found: %s", jsonV))
}
}
testCase.LogReq(request)
testCase.LogResp(apiResponse)
return false, apiResponse, nil
}
if testCase.WaitAfter != nil {
if testCase.LogShort == nil || !*testCase.LogShort {
logrus.Infof("wait_after_ms: %d", *testCase.WaitAfter)
}
time.Sleep(time.Duration(*testCase.WaitAfter) * time.Millisecond)
}
return true, apiResponse, nil
}
func (testCase Case) loadRequest() (api.Request, error) {
req, err := testCase.loadRequestSerialization()
if err != nil {
return req, fmt.Errorf("error loadRequestSerialization: %s", err)
}
return req, err
}
func (testCase Case) loadExpectedResponse() (res api.Response, err error) {
// unspecified response is interpreted as status_code 200
if testCase.ResponseData == nil {
return api.NewResponse(http.StatusOK, nil, nil, nil, nil, res.Format)
}
spec, err := testCase.loadResponseSerialization(testCase.ResponseData)
if err != nil {
return res, fmt.Errorf("error loading response spec: %s", err)
}
res, err = api.NewResponseFromSpec(spec)
if err != nil {
return res, fmt.Errorf("error creating response from spec: %s", err)
}
return res, nil
}
func (testCase Case) responsesEqual(expected, got api.Response) (compare.CompareResult, error) {
expectedJSON, err := expected.ToGenericJSON()
if err != nil {
return compare.CompareResult{}, fmt.Errorf("error loading expected generic json: %s", err)
}
if len(expected.Body) == 0 && len(expected.BodyControl) == 0 {
expected.Format.IgnoreBody = true
} else {
expected.Format.IgnoreBody = false
}
gotJSON, err := got.ServerResponseToGenericJSON(expected.Format, false)
if err != nil {
return compare.CompareResult{}, fmt.Errorf("error loading response generic json: %s", err)
}
return compare.JsonEqual(expectedJSON, gotJSON, compare.ComparisonContext{})
}
func (testCase Case) loadRequestSerialization() (api.Request, error) {
var (
spec api.Request
)
reqLoader := testCase.loader
_, requestData, err := template.LoadManifestDataAsObject(*testCase.RequestData, testCase.manifestDir, reqLoader)
if err != nil {
return spec, fmt.Errorf("error loading request data: %s", err)
}
specBytes, err := json.Marshal(requestData)
if err != nil {
return spec, fmt.Errorf("error marshaling req: %s", err)
}
err = util.Unmarshal(specBytes, &spec)
spec.ManifestDir = testCase.manifestDir
spec.DataStore = testCase.dataStore
if spec.ServerURL == "" {
spec.ServerURL = testCase.ServerURL
}
if len(spec.Headers) == 0 {
spec.Headers = make(map[string]any)
}
for k, v := range testCase.standardHeader {
if _, exist := spec.Headers[k]; !exist {
spec.Headers[k] = v
}
}
if len(spec.HeaderFromStore) == 0 {
spec.HeaderFromStore = make(map[string]string)
}
for k, v := range testCase.standardHeaderFromStore {
if _, exist := spec.HeaderFromStore[k]; !exist {
spec.HeaderFromStore[k] = v
}
}
return spec, nil
}
func (testCase Case) loadResponseSerialization(genJSON any) (spec api.ResponseSerialization, err error) {
resLoader := testCase.loader
_, responseData, err := template.LoadManifestDataAsObject(genJSON, testCase.manifestDir, resLoader)
if err != nil {
return spec, fmt.Errorf("error loading response data: %s", err)
}
specBytes, err := json.Marshal(responseData)
if err != nil {
return spec, fmt.Errorf("error marshaling res: %s", err)
}
err = util.Unmarshal(specBytes, &spec)
if err != nil {
return spec, fmt.Errorf("error unmarshaling res: %s", err)
}
// the body must not be parsed if it is not expected in the response, or should not be stored
if spec.Body == nil && len(testCase.StoreResponse) == 0 {
spec.Format.IgnoreBody = true
}
return spec, nil
}