Skip to content

Commit 010adb5

Browse files
Task/http assert json template (#292)
## What - http assert json template ## Why <!-- Describe why are these changes necessary? --> ## References <!-- Add identifier for issue tickets, links to other PRs, etc. --> ## Checklist <!-- Remove this section if not applicable to your changes --> - [ ] Tests
1 parent b33580b commit 010adb5

File tree

8 files changed

+349
-147
lines changed

8 files changed

+349
-147
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ require (
1616
github.com/opensearch-project/opensearch-go/v4 v4.6.0
1717
github.com/peterldowns/pgtestdb v0.1.1
1818
github.com/peterldowns/pgtestdb/migrators/golangmigrator v0.1.1
19+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
1920
github.com/rs/zerolog v1.34.0
2021
github.com/samber/lo v1.52.0
2122
github.com/spf13/viper v1.21.0
2223
github.com/stretchr/testify v1.11.1
2324
github.com/swaggo/files/v2 v2.0.2
2425
github.com/swaggo/swag v1.16.6
2526
github.com/testcontainers/testcontainers-go v0.40.0
27+
github.com/tidwall/gjson v1.18.0
2628
github.com/tidwall/sjson v1.2.5
2729
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0
2830
golang.org/x/crypto v0.48.0
@@ -108,7 +110,6 @@ require (
108110
github.com/opencontainers/image-spec v1.1.1 // indirect
109111
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
110112
github.com/pkg/errors v0.9.1 // indirect
111-
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
112113
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
113114
github.com/quic-go/qpack v0.6.0 // indirect
114115
github.com/quic-go/quic-go v0.59.0 // indirect
@@ -120,7 +121,6 @@ require (
120121
github.com/spf13/pflag v1.0.10 // indirect
121122
github.com/stretchr/objx v0.5.3 // indirect
122123
github.com/subosito/gotenv v1.6.0 // indirect
123-
github.com/tidwall/gjson v1.18.0 // indirect
124124
github.com/tidwall/match v1.2.0 // indirect
125125
github.com/tidwall/pretty v1.2.1 // indirect
126126
github.com/tklauser/go-sysconf v0.3.16 // indirect

pkg/httpassert/README.md

Lines changed: 81 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@ import "github.com/greenbone/opensight-golang-libraries/pkg/httpassert"
1111
## Index
1212

1313
- [Constants](<#constants>)
14+
- [func AssertJSONCanonicalEq\(t \*testing.T, expected, actual string\) bool](<#AssertJSONCanonicalEq>)
15+
- [func NormalizeJSON\(t \*testing.T, s string\) string](<#NormalizeJSON>)
1416
- [type Extractor](<#Extractor>)
1517
- [func ExtractRegexTo\(value string, ptr any\) Extractor](<#ExtractRegexTo>)
1618
- [func ExtractTo\(ptr any\) Extractor](<#ExtractTo>)
1719
- [type Matcher](<#Matcher>)
1820
- [func Contains\(v string\) Matcher](<#Contains>)
1921
- [func HasSize\(e int\) Matcher](<#HasSize>)
22+
- [func IsUUID\(\) Matcher](<#IsUUID>)
23+
- [func NotEmpty\(\) Matcher](<#NotEmpty>)
2024
- [func Regex\(expr string\) Matcher](<#Regex>)
2125
- [type Request](<#Request>)
22-
- [func New\(t \*testing.T, router http.Handler\) Request](<#New>)
26+
- [type RequestStart](<#RequestStart>)
27+
- [func New\(t \*testing.T, router http.Handler\) RequestStart](<#New>)
2328
- [type Response](<#Response>)
2429

2530

@@ -31,6 +36,24 @@ import "github.com/greenbone/opensight-golang-libraries/pkg/httpassert"
3136
const IgnoreJsonValue = "<IGNORE>"
3237
```
3338

39+
<a name="AssertJSONCanonicalEq"></a>
40+
## func [AssertJSONCanonicalEq](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/assert.go#L32>)
41+
42+
```go
43+
func AssertJSONCanonicalEq(t *testing.T, expected, actual string) bool
44+
```
45+
46+
AssertJSONCanonicalEq compares two JSON strings by normalizing both first. On mismatch, it prints a readable diff of the normalized forms.
47+
48+
<a name="NormalizeJSON"></a>
49+
## func [NormalizeJSON](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/assert.go#L15>)
50+
51+
```go
52+
func NormalizeJSON(t *testing.T, s string) string
53+
```
54+
55+
NormalizeJSON parses JSON and re\-marshals it with stable key ordering and indentation. \- Uses Decoder.UseNumber\(\) to preserve numeric fidelity \(avoid float64 surprises\).
56+
3457
<a name="Extractor"></a>
3558
## type [Extractor](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/extracter.go#L16>)
3659

@@ -64,7 +87,7 @@ request.Expect().JsonPath("$.data.id", httpassert.ExtractTo(&id))
6487
```
6588

6689
<a name="Matcher"></a>
67-
## type [Matcher](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L15>)
90+
## type [Matcher](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L16>)
6891

6992

7093

@@ -73,39 +96,84 @@ type Matcher func(t *testing.T, actual any) bool
7396
```
7497

7598
<a name="Contains"></a>
76-
### func [Contains](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L43>)
99+
### func [Contains](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L44>)
77100

78101
```go
79102
func Contains(v string) Matcher
80103
```
81104

82-
Contains checks if a string contains the value Example: ExpectJsonPath\("$.data.name", httpassert.Contains\("foo"\)\)
105+
Contains checks if a string contains the value Example: JsonPath\("$.data.name", httpassert.Contains\("foo"\)\)
83106

84107
<a name="HasSize"></a>
85-
### func [HasSize](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L19>)
108+
### func [HasSize](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L20>)
86109

87110
```go
88111
func HasSize(e int) Matcher
89112
```
90113

91-
HasSize checks the length of arrays, maps, or strings. Example: ExpectJsonPath\("$.data", httpassert.HasSize\(11\)\)
114+
HasSize checks the length of arrays, maps, or strings. Example: JsonPath\("$.data", httpassert.HasSize\(11\)\)
115+
116+
<a name="IsUUID"></a>
117+
### func [IsUUID](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L76>)
118+
119+
```go
120+
func IsUUID() Matcher
121+
```
122+
123+
IsUUID checks if a string is a UUID Example: JsonPath\("$.id", httpassert.IsUUID\(\)\)
124+
125+
<a name="NotEmpty"></a>
126+
### func [NotEmpty](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L63>)
127+
128+
```go
129+
func NotEmpty() Matcher
130+
```
131+
132+
NotEmpty checks if a string is not empty Example: JsonPath\("$.data.name", httpassert.NotEmpty\(\)\)
92133

93134
<a name="Regex"></a>
94-
### func [Regex](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L52>)
135+
### func [Regex](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L53>)
95136

96137
```go
97138
func Regex(expr string) Matcher
98139
```
99140

100-
Regex checks if a string matches the given regular expression Example: ExpectJsonPath\("$.data.name", httpassert.Regex\("^foo.\*bar$"\)\)
141+
Regex checks if a string matches the given regular expression Example: JsonPath\("$.data.name", httpassert.Regex\("^foo.\*bar$"\)\)
101142

102143
<a name="Request"></a>
103-
## type [Request](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/request.go#L23-L55>)
144+
## type [Request](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/request.go#L47-L65>)
104145

105-
nolint:interfacebloat Request interface provides fluent HTTP request building.
146+
nolint:interfacebloat Request provides fluent request configuration
106147

107148
```go
108149
type Request interface {
150+
AuthHeader(header string) Request
151+
Headers(headers map[string]string) Request
152+
Header(key, value string) Request
153+
AuthJwt(jwt string) Request
154+
155+
ContentType(string) Request
156+
157+
Content(string) Request
158+
ContentFile(string) Request
159+
160+
JsonContent(string) Request
161+
JsonContentTemplate(body string, values map[string]any) Request
162+
JsonContentObject(any) Request
163+
JsonContentFile(path string) Request
164+
165+
Expect() Response
166+
ExpectEventually(check func(r Response), timeout time.Duration, interval time.Duration) Response
167+
}
168+
```
169+
170+
<a name="RequestStart"></a>
171+
## type [RequestStart](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/request.go#L27-L43>)
172+
173+
nolint:interfacebloat RequestStart provides fluent HTTP \*method \+ path\* selection. Each call returns a fresh Request
174+
175+
```go
176+
type RequestStart interface {
109177
Get(path string) Request
110178
Getf(format string, a ...interface{}) Request
111179
Post(path string) Request
@@ -121,33 +189,17 @@ type Request interface {
121189

122190
Perform(verb string, path string) Request
123191
Performf(verb string, path string, a ...interface{}) Request
124-
125-
AuthHeader(header string) Request
126-
Headers(headers map[string]string) Request
127-
Header(key, value string) Request
128-
AuthJwt(jwt string) Request
129-
130-
ContentType(string) Request
131-
132-
Content(string) Request
133-
JsonContent(string) Request
134-
JsonContentObject(any) Request
135-
ContentFile(string) Request
136-
JsonContentFile(path string) Request
137-
138-
Expect() Response
139-
ExpectEventually(check func(r Response), timeout time.Duration, interval time.Duration) Response
140192
}
141193
```
142194

143195
<a name="New"></a>
144-
### func [New](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/request.go#L119>)
196+
### func [New](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/request.go#L95>)
145197

146198
```go
147-
func New(t *testing.T, router http.Handler) Request
199+
func New(t *testing.T, router http.Handler) RequestStart
148200
```
149201

150-
New returns a new Request instance for the given router.
202+
New returns a new RequestStart instance for the given router. All method calls \(Get/Post/...\) return a \*fresh\* Request.
151203

152204
<a name="Response"></a>
153205
## type [Response](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/response.go#L24-L46>)

pkg/httpassert/assert.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package httpassert
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"testing"
7+
8+
"github.com/pmezard/go-difflib/difflib"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// NormalizeJSON parses JSON and re-marshals it with stable key ordering and indentation.
14+
// - Uses Decoder.UseNumber() to preserve numeric fidelity (avoid float64 surprises).
15+
func NormalizeJSON(t *testing.T, s string) string {
16+
t.Helper()
17+
18+
dec := json.NewDecoder(bytes.NewReader([]byte(s)))
19+
dec.UseNumber()
20+
21+
var v any
22+
require.NoError(t, dec.Decode(&v), "invalid JSON")
23+
24+
b, err := json.MarshalIndent(v, "", " ")
25+
require.NoError(t, err, "failed to marshal normalized JSON")
26+
27+
return string(b)
28+
}
29+
30+
// AssertJSONCanonicalEq compares two JSON strings by normalizing both first.
31+
// On mismatch, it prints a readable diff of the normalized forms.
32+
func AssertJSONCanonicalEq(t *testing.T, expected, actual string) bool {
33+
t.Helper()
34+
35+
expNorm := NormalizeJSON(t, expected)
36+
actNorm := NormalizeJSON(t, actual)
37+
38+
if expNorm != actNorm {
39+
diff := difflib.UnifiedDiff{
40+
A: difflib.SplitLines(expNorm),
41+
B: difflib.SplitLines(actNorm),
42+
FromFile: "Expected",
43+
ToFile: "Actual",
44+
Context: 3,
45+
}
46+
47+
text, _ := difflib.GetUnifiedDiffString(diff)
48+
49+
return assert.Fail(t, "JSON mismatch:\nDiff:\n"+text)
50+
}
51+
return true
52+
}

pkg/httpassert/matcher.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ import (
99
"regexp"
1010
"testing"
1111

12+
"github.com/google/uuid"
1213
"github.com/stretchr/testify/assert"
1314
)
1415

1516
type Matcher func(t *testing.T, actual any) bool
1617

1718
// HasSize checks the length of arrays, maps, or strings.
18-
// Example: ExpectJsonPath("$.data", httpassert.HasSize(11))
19+
// Example: JsonPath("$.data", httpassert.HasSize(11))
1920
func HasSize(e int) Matcher {
2021
return func(t *testing.T, actual any) bool {
2122
var size int
@@ -39,7 +40,7 @@ func HasSize(e int) Matcher {
3940
}
4041

4142
// Contains checks if a string contains the value
42-
// Example: ExpectJsonPath("$.data.name", httpassert.Contains("foo"))
43+
// Example: JsonPath("$.data.name", httpassert.Contains("foo"))
4344
func Contains(v string) Matcher {
4445
return func(t *testing.T, value any) bool {
4546
valid := assert.Contains(t, value, v)
@@ -48,7 +49,7 @@ func Contains(v string) Matcher {
4849
}
4950

5051
// Regex checks if a string matches the given regular expression
51-
// Example: ExpectJsonPath("$.data.name", httpassert.Regex("^foo.*bar$"))
52+
// Example: JsonPath("$.data.name", httpassert.Regex("^foo.*bar$"))
5253
func Regex(expr string) Matcher {
5354
re := regexp.MustCompile(expr)
5455

@@ -58,7 +59,7 @@ func Regex(expr string) Matcher {
5859
}
5960

6061
// NotEmpty checks if a string is not empty
61-
// Example: ExpectJsonPath("$.data.name", httpassert.NotEmpty())
62+
// Example: JsonPath("$.data.name", httpassert.NotEmpty())
6263
func NotEmpty() Matcher {
6364
return func(t *testing.T, value any) bool {
6465
str, ok := value.(string)
@@ -69,3 +70,23 @@ func NotEmpty() Matcher {
6970
return assert.NotEmpty(t, str)
7071
}
7172
}
73+
74+
// IsUUID checks if a string is a UUID
75+
// Example: JsonPath("$.id", httpassert.IsUUID())
76+
func IsUUID() Matcher {
77+
return func(t *testing.T, value any) bool {
78+
t.Helper()
79+
80+
str, ok := value.(string)
81+
if !ok {
82+
return assert.Fail(t, "value is not a string")
83+
}
84+
85+
_, err := uuid.Parse(str)
86+
if err != nil {
87+
return assert.Fail(t, "value is not a valid UUID", "'%s': %v", str, err)
88+
}
89+
90+
return true
91+
}
92+
}

pkg/httpassert/matcher_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net/http"
1010
"testing"
1111

12+
"github.com/google/uuid"
1213
"github.com/stretchr/testify/assert"
1314
)
1415

@@ -56,3 +57,18 @@ func Test_NoEmptyMatcher(t *testing.T) {
5657
StatusCode(http.StatusOK).
5758
JsonPath("$.data.name", NotEmpty())
5859
}
60+
61+
func Test_IsUUIDMatcher(t *testing.T) {
62+
router := http.NewServeMux()
63+
router.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
64+
_, err := fmt.Fprintf(w, `{"id": "%s"}`, uuid.New())
65+
assert.NoError(t, err)
66+
})
67+
68+
request := New(t, router)
69+
70+
request.Get("/api").
71+
Expect().
72+
StatusCode(http.StatusOK).
73+
JsonPath("$.id", IsUUID())
74+
}

0 commit comments

Comments
 (0)