Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ require (
github.com/opensearch-project/opensearch-go/v4 v4.6.0
github.com/peterldowns/pgtestdb v0.1.1
github.com/peterldowns/pgtestdb/migrators/golangmigrator v0.1.1
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
github.com/rs/zerolog v1.34.0
github.com/samber/lo v1.52.0
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/swaggo/files/v2 v2.0.2
github.com/swaggo/swag v1.16.6
github.com/testcontainers/testcontainers-go v0.40.0
github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0
golang.org/x/crypto v0.48.0
Expand Down Expand Up @@ -108,7 +110,6 @@ require (
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
Expand All @@ -120,7 +121,6 @@ require (
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
Expand Down
110 changes: 81 additions & 29 deletions pkg/httpassert/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ import "github.com/greenbone/opensight-golang-libraries/pkg/httpassert"
## Index

- [Constants](<#constants>)
- [func AssertJSONCanonicalEq\(t \*testing.T, expected, actual string\) bool](<#AssertJSONCanonicalEq>)
- [func NormalizeJSON\(t \*testing.T, s string\) string](<#NormalizeJSON>)
- [type Extractor](<#Extractor>)
- [func ExtractRegexTo\(value string, ptr any\) Extractor](<#ExtractRegexTo>)
- [func ExtractTo\(ptr any\) Extractor](<#ExtractTo>)
- [type Matcher](<#Matcher>)
- [func Contains\(v string\) Matcher](<#Contains>)
- [func HasSize\(e int\) Matcher](<#HasSize>)
- [func IsUUID\(\) Matcher](<#IsUUID>)
- [func NotEmpty\(\) Matcher](<#NotEmpty>)
- [func Regex\(expr string\) Matcher](<#Regex>)
- [type Request](<#Request>)
- [func New\(t \*testing.T, router http.Handler\) Request](<#New>)
- [type RequestStart](<#RequestStart>)
- [func New\(t \*testing.T, router http.Handler\) RequestStart](<#New>)
- [type Response](<#Response>)


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

<a name="AssertJSONCanonicalEq"></a>
## func [AssertJSONCanonicalEq](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/assert.go#L32>)

```go
func AssertJSONCanonicalEq(t *testing.T, expected, actual string) bool
```

AssertJSONCanonicalEq compares two JSON strings by normalizing both first. On mismatch, it prints a readable diff of the normalized forms.

<a name="NormalizeJSON"></a>
## func [NormalizeJSON](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/assert.go#L15>)

```go
func NormalizeJSON(t *testing.T, s string) string
```

NormalizeJSON parses JSON and re\-marshals it with stable key ordering and indentation. \- Uses Decoder.UseNumber\(\) to preserve numeric fidelity \(avoid float64 surprises\).

<a name="Extractor"></a>
## type [Extractor](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/extracter.go#L16>)

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

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



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

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

```go
func Contains(v string) Matcher
```

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

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

```go
func HasSize(e int) Matcher
```

HasSize checks the length of arrays, maps, or strings. Example: ExpectJsonPath\("$.data", httpassert.HasSize\(11\)\)
HasSize checks the length of arrays, maps, or strings. Example: JsonPath\("$.data", httpassert.HasSize\(11\)\)

<a name="IsUUID"></a>
### func [IsUUID](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L76>)

```go
func IsUUID() Matcher
```

IsUUID checks if a string is a UUID Example: JsonPath\("$.id", httpassert.IsUUID\(\)\)

<a name="NotEmpty"></a>
### func [NotEmpty](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/matcher.go#L63>)

```go
func NotEmpty() Matcher
```

NotEmpty checks if a string is not empty Example: JsonPath\("$.data.name", httpassert.NotEmpty\(\)\)

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

```go
func Regex(expr string) Matcher
```

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

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

nolint:interfacebloat Request interface provides fluent HTTP request building.
nolint:interfacebloat Request provides fluent request configuration

```go
type Request interface {
AuthHeader(header string) Request
Headers(headers map[string]string) Request
Header(key, value string) Request
AuthJwt(jwt string) Request

ContentType(string) Request

Content(string) Request
ContentFile(string) Request

JsonContent(string) Request
JsonContentTemplate(body string, values map[string]any) Request
JsonContentObject(any) Request
JsonContentFile(path string) Request

Expect() Response
ExpectEventually(check func(r Response), timeout time.Duration, interval time.Duration) Response
}
```

<a name="RequestStart"></a>
## type [RequestStart](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/request.go#L27-L43>)

nolint:interfacebloat RequestStart provides fluent HTTP \*method \+ path\* selection. Each call returns a fresh Request

```go
type RequestStart interface {
Get(path string) Request
Getf(format string, a ...interface{}) Request
Post(path string) Request
Expand All @@ -121,33 +189,17 @@ type Request interface {

Perform(verb string, path string) Request
Performf(verb string, path string, a ...interface{}) Request

AuthHeader(header string) Request
Headers(headers map[string]string) Request
Header(key, value string) Request
AuthJwt(jwt string) Request

ContentType(string) Request

Content(string) Request
JsonContent(string) Request
JsonContentObject(any) Request
ContentFile(string) Request
JsonContentFile(path string) Request

Expect() Response
ExpectEventually(check func(r Response), timeout time.Duration, interval time.Duration) Response
}
```

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

```go
func New(t *testing.T, router http.Handler) Request
func New(t *testing.T, router http.Handler) RequestStart
```

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

<a name="Response"></a>
## type [Response](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/httpassert/response.go#L24-L46>)
Expand Down
52 changes: 52 additions & 0 deletions pkg/httpassert/assert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package httpassert

import (
"bytes"
"encoding/json"
"testing"

"github.com/pmezard/go-difflib/difflib"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// NormalizeJSON parses JSON and re-marshals it with stable key ordering and indentation.
// - Uses Decoder.UseNumber() to preserve numeric fidelity (avoid float64 surprises).
func NormalizeJSON(t *testing.T, s string) string {
t.Helper()

dec := json.NewDecoder(bytes.NewReader([]byte(s)))
dec.UseNumber()

var v any
require.NoError(t, dec.Decode(&v), "invalid JSON")

b, err := json.MarshalIndent(v, "", " ")
require.NoError(t, err, "failed to marshal normalized JSON")

return string(b)
}

// AssertJSONCanonicalEq compares two JSON strings by normalizing both first.
// On mismatch, it prints a readable diff of the normalized forms.
func AssertJSONCanonicalEq(t *testing.T, expected, actual string) bool {
t.Helper()

expNorm := NormalizeJSON(t, expected)
actNorm := NormalizeJSON(t, actual)

if expNorm != actNorm {
diff := difflib.UnifiedDiff{
A: difflib.SplitLines(expNorm),
B: difflib.SplitLines(actNorm),
FromFile: "Expected",
ToFile: "Actual",
Context: 3,
}

text, _ := difflib.GetUnifiedDiffString(diff)

return assert.Fail(t, "JSON mismatch:\nDiff:\n"+text)
}
return true
}
29 changes: 25 additions & 4 deletions pkg/httpassert/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import (
"regexp"
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)

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

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

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

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

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

// NotEmpty checks if a string is not empty
// Example: ExpectJsonPath("$.data.name", httpassert.NotEmpty())
// Example: JsonPath("$.data.name", httpassert.NotEmpty())
func NotEmpty() Matcher {
return func(t *testing.T, value any) bool {
str, ok := value.(string)
Expand All @@ -69,3 +70,23 @@ func NotEmpty() Matcher {
return assert.NotEmpty(t, str)
}
}

// IsUUID checks if a string is a UUID
// Example: JsonPath("$.id", httpassert.IsUUID())
func IsUUID() Matcher {
return func(t *testing.T, value any) bool {
t.Helper()

str, ok := value.(string)
if !ok {
return assert.Fail(t, "value is not a string")
}

_, err := uuid.Parse(str)
if err != nil {
return assert.Fail(t, "value is not a valid UUID", "'%s': %v", str, err)
}

return true
}
}
16 changes: 16 additions & 0 deletions pkg/httpassert/matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -56,3 +57,18 @@ func Test_NoEmptyMatcher(t *testing.T) {
StatusCode(http.StatusOK).
JsonPath("$.data.name", NotEmpty())
}

func Test_IsUUIDMatcher(t *testing.T) {
router := http.NewServeMux()
router.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
_, err := fmt.Fprintf(w, `{"id": "%s"}`, uuid.New())
assert.NoError(t, err)
})

request := New(t, router)

request.Get("/api").
Expect().
StatusCode(http.StatusOK).
JsonPath("$.id", IsUUID())
}
Loading
Loading