Skip to content

Commit 3231e1b

Browse files
Add httpassert (#270)
## What - add http assert lib from report team ## Why - we use this in the report team and we need that for the notification service too ## Checklist <!-- Remove this section if not applicable to your changes --> - [x] Tests
1 parent f9719d7 commit 3231e1b

File tree

12 files changed

+1333
-0
lines changed

12 files changed

+1333
-0
lines changed

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ require (
2323
github.com/swaggo/files/v2 v2.0.2
2424
github.com/swaggo/swag v1.16.6
2525
github.com/testcontainers/testcontainers-go v0.40.0
26+
github.com/tidwall/sjson v1.2.5
27+
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0
2628
gorm.io/driver/postgres v1.6.0
2729
gorm.io/driver/sqlite v1.6.0
2830
gorm.io/gorm v1.31.1
@@ -118,6 +120,9 @@ require (
118120
github.com/spf13/pflag v1.0.10 // indirect
119121
github.com/stretchr/objx v0.5.3 // indirect
120122
github.com/subosito/gotenv v1.6.0 // indirect
123+
github.com/tidwall/gjson v1.18.0 // indirect
124+
github.com/tidwall/match v1.1.1 // indirect
125+
github.com/tidwall/pretty v1.2.1 // indirect
121126
github.com/tklauser/go-sysconf v0.3.16 // indirect
122127
github.com/tklauser/numcpus v0.11.0 // indirect
123128
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,12 @@ github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
282282
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
283283
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
284284
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
285+
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
285286
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
286287
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
287288
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
288289
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
290+
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
289291
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
290292
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
291293
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
@@ -300,6 +302,8 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY
300302
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
301303
github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ=
302304
github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM=
305+
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
306+
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
303307
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
304308
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
305309
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=

pkg/httpassert/README.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<!-- gomarkdoc:embed:start -->
2+
3+
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
4+
5+
# httpassert
6+
7+
```go
8+
import "github.com/greenbone/opensight-golang-libraries/pkg/httpassert"
9+
```
10+
11+
## Index
12+
13+
- [Constants](<#constants>)
14+
- [type Extractor](<#Extractor>)
15+
- [func ExtractTo\(ptr any\) Extractor](<#ExtractTo>)
16+
- [type Matcher](<#Matcher>)
17+
- [func Contains\(v string\) Matcher](<#Contains>)
18+
- [func HasSize\(e int\) Matcher](<#HasSize>)
19+
- [type Request](<#Request>)
20+
- [func New\(t \*testing.T, router http.Handler\) Request](<#New>)
21+
- [type Response](<#Response>)
22+
23+
24+
## Constants
25+
26+
<a name="IgnoreJsonValue"></a>
27+
28+
```go
29+
const IgnoreJsonValue = "<IGNORE>"
30+
```
31+
32+
<a name="Extractor"></a>
33+
## type [Extractor](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/extracter.go#L15>)
34+
35+
36+
37+
```go
38+
type Extractor func(t *testing.T, actual any) any
39+
```
40+
41+
<a name="ExtractTo"></a>
42+
### func [ExtractTo](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/extracter.go#L22>)
43+
44+
```go
45+
func ExtractTo(ptr any) Extractor
46+
```
47+
48+
ExtractTo sets the value read from JSONPath into the given pointer variable. Example:
49+
50+
```
51+
var id string
52+
request.Expect().JsonPath("$.data.id", httpassert.ExtractTo(&id))
53+
```
54+
55+
<a name="Matcher"></a>
56+
## type [Matcher](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L14>)
57+
58+
59+
60+
```go
61+
type Matcher func(t *testing.T, actual any) bool
62+
```
63+
64+
<a name="Contains"></a>
65+
### func [Contains](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L42>)
66+
67+
```go
68+
func Contains(v string) Matcher
69+
```
70+
71+
Contains checks if a string contains the value Example: ExpectJsonPath\("$.data.name", httpassert.Contains\("foo"\)\)
72+
73+
<a name="HasSize"></a>
74+
### func [HasSize](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L18>)
75+
76+
```go
77+
func HasSize(e int) Matcher
78+
```
79+
80+
HasSize checks the length of arrays, maps, or strings. Example: ExpectJsonPath\("$.data", httpassert.HasSize\(11\)\)
81+
82+
<a name="Request"></a>
83+
## type [Request](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/request.go#L23-L55>)
84+
85+
nolint:interfacebloat Request interface provides fluent HTTP request building.
86+
87+
```go
88+
type Request interface {
89+
Get(path string) Request
90+
Getf(format string, a ...interface{}) Request
91+
Post(path string) Request
92+
Postf(format string, a ...interface{}) Request
93+
Put(path string) Request
94+
Putf(format string, a ...interface{}) Request
95+
Delete(path string) Request
96+
Deletef(format string, a ...interface{}) Request
97+
Options(path string) Request
98+
Optionsf(format string, a ...interface{}) Request
99+
Patch(path string) Request
100+
Patchf(format string, a ...interface{}) Request
101+
102+
Perform(verb string, path string) Request
103+
Performf(verb string, path string, a ...interface{}) Request
104+
105+
AuthHeader(header string) Request
106+
Headers(headers map[string]string) Request
107+
Header(key, value string) Request
108+
AuthJwt(jwt string) Request
109+
110+
ContentType(string) Request
111+
112+
Content(string) Request
113+
JsonContent(string) Request
114+
JsonContentObject(any) Request
115+
ContentFile(string) Request
116+
JsonContentFile(path string) Request
117+
118+
Expect() Response
119+
ExpectEventually(check func(r Response), timeout time.Duration, interval time.Duration) Response
120+
}
121+
```
122+
123+
<a name="New"></a>
124+
### func [New](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/request.go#L119>)
125+
126+
```go
127+
func New(t *testing.T, router http.Handler) Request
128+
```
129+
130+
New returns a new Request instance for the given router.
131+
132+
<a name="Response"></a>
133+
## type [Response](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/response.go#L24-L44>)
134+
135+
nolint:interfacebloat Response interface provides fluent response assertions.
136+
137+
```go
138+
type Response interface {
139+
StatusCode(int) Response
140+
141+
JsonPath(string, any) Response
142+
JsonPathJson(path string, expectedJson string) Response
143+
144+
ContentType(contentType string) Response
145+
146+
NoContent() Response
147+
148+
Json(json string) Response
149+
JsonTemplate(json string, values map[string]any) Response
150+
JsonTemplateFile(path string, values map[string]any) Response
151+
JsonFile(path string) Response
152+
153+
Body(body string) Response
154+
GetJsonBodyObject(target any) Response
155+
GetBody() string
156+
157+
Log() Response
158+
}
159+
```
160+
161+
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
162+
163+
164+
<!-- gomarkdoc:embed:end -->

pkg/httpassert/extracter.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// SPDX-FileCopyrightText: 2025 Greenbone AG
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
package httpassert
6+
7+
import (
8+
"fmt"
9+
"reflect"
10+
"testing"
11+
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
type Extractor func(t *testing.T, actual any) any
16+
17+
// ExtractTo sets the value read from JSONPath into the given pointer variable.
18+
// Example:
19+
//
20+
// var id string
21+
// request.Expect().JsonPath("$.data.id", httpassert.ExtractTo(&id))
22+
func ExtractTo(ptr any) Extractor {
23+
return func(t *testing.T, actual any) any {
24+
targetVal := reflect.ValueOf(ptr)
25+
if targetVal.Kind() != reflect.Ptr || targetVal.IsNil() {
26+
assert.Fail(t, "ExtractTo requires a non-nil pointer")
27+
return nil
28+
}
29+
30+
if actual == nil {
31+
assert.Fail(t, "ExtractTo actual value is nil")
32+
return nil
33+
}
34+
35+
outVal := reflect.ValueOf(actual)
36+
targetType := targetVal.Elem().Type()
37+
38+
// Direct assign / convert for simple values (string, int, etc.)
39+
if outVal.Type().AssignableTo(targetType) {
40+
targetVal.Elem().Set(outVal)
41+
return ptr
42+
}
43+
44+
if outVal.Type().ConvertibleTo(targetType) {
45+
targetVal.Elem().Set(outVal.Convert(targetType))
46+
return ptr
47+
}
48+
49+
// Special handling for slices, e.g. []interface{} -> []string
50+
if outVal.Kind() == reflect.Slice && targetType.Kind() == reflect.Slice {
51+
elemType := targetType.Elem()
52+
n := outVal.Len()
53+
dst := reflect.MakeSlice(targetType, n, n)
54+
55+
for i := 0; i < n; i++ {
56+
src := outVal.Index(i)
57+
58+
// If it's interface{}, unwrap to the underlying concrete value.
59+
if src.Kind() == reflect.Interface && !src.IsNil() {
60+
src = reflect.ValueOf(src.Interface())
61+
}
62+
63+
if src.Type().AssignableTo(elemType) {
64+
dst.Index(i).Set(src)
65+
continue
66+
}
67+
68+
if src.Type().ConvertibleTo(elemType) {
69+
dst.Index(i).Set(src.Convert(elemType))
70+
continue
71+
}
72+
73+
assert.Fail(t,
74+
fmt.Sprintf("ExtractTo slice element type mismatch at index %d: cannot assign %v to %v",
75+
i, src.Type(), elemType))
76+
return nil
77+
}
78+
79+
targetVal.Elem().Set(dst)
80+
return ptr
81+
}
82+
83+
assert.Fail(t,
84+
fmt.Sprintf("ExtractTo type mismatch: cannot assign %v to %v",
85+
outVal.Type(), targetType))
86+
return nil
87+
}
88+
}

pkg/httpassert/extracter_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// SPDX-FileCopyrightText: 2025 Greenbone AG
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
package httpassert
6+
7+
import (
8+
"fmt"
9+
"net/http"
10+
"testing"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestExtractTo(t *testing.T) {
17+
router := http.NewServeMux()
18+
router.HandleFunc("/data", func(w http.ResponseWriter, r *http.Request) {
19+
_, err := fmt.Fprint(w, `{"data":{
20+
"id":"abc-123",
21+
"ids":["a", "b"],
22+
"number":123 ,
23+
"numbers":[1,2]
24+
}}`)
25+
assert.NoError(t, err)
26+
})
27+
28+
request := New(t, router)
29+
30+
t.Run("string", func(t *testing.T) {
31+
var id string
32+
request.Post("/data").
33+
Content(`{"name":"appliance1"}`).
34+
Expect().
35+
StatusCode(http.StatusOK).
36+
JsonPath("$.data.id", ExtractTo(&id))
37+
38+
require.NotEmpty(t, id)
39+
require.Equal(t, "abc-123", id)
40+
})
41+
42+
t.Run("string list", func(t *testing.T) {
43+
var ids []string
44+
request.Post("/data").
45+
Content(`{"name":"appliance1"}`).
46+
Expect().
47+
StatusCode(http.StatusOK).
48+
JsonPath("$.data.ids", ExtractTo(&ids))
49+
50+
require.NotEmpty(t, ids)
51+
require.Equal(t, []string{"a", "b"}, ids)
52+
})
53+
54+
t.Run("number", func(t *testing.T) {
55+
var number int
56+
request.Post("/data").
57+
Content(`{"name":"appliance1"}`).
58+
Expect().
59+
StatusCode(http.StatusOK).
60+
JsonPath("$.data.number", ExtractTo(&number))
61+
62+
require.NotEmpty(t, number)
63+
require.Equal(t, 123, number)
64+
})
65+
66+
t.Run("number list", func(t *testing.T) {
67+
var numbers []int
68+
request.Post("/data").
69+
Content(`{"name":"appliance1"}`).
70+
Expect().
71+
StatusCode(http.StatusOK).
72+
JsonPath("$.data.numbers", ExtractTo(&numbers))
73+
74+
require.NotEmpty(t, numbers)
75+
require.Equal(t, []int{1, 2}, numbers)
76+
})
77+
}

0 commit comments

Comments
 (0)