Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
trapajim committed Apr 12, 2024
1 parent 9919a36 commit e8d3737
Show file tree
Hide file tree
Showing 8 changed files with 680 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ jobs:
needs: pre_job
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: test
uses: trapajim/go-pipeline-action@v1
with:
Expand Down
116 changes: 116 additions & 0 deletions assertions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package testpilot

import (
"bufio"
"encoding/json"
"errors"
"fmt"
"github.com/google/go-cmp/cmp"
"reflect"
"strings"
)

type AssertionFunc func(body []byte) error

// AssertEqual asserts that the response body is equal to the given value
// It uses reflect.DeepEqual to compare the response body with the given value
func AssertEqual(response any) AssertionFunc {
return func(body []byte) error {
t := reflect.TypeOf(response)
if t.Kind() == reflect.Ptr {
t = t.Elem() // If it's a pointer, get the type it points to
}
newVar := reflect.New(t).Interface()
var got any
if t.Kind() == reflect.Struct || t.Kind() == reflect.Map || t.Kind() == reflect.Slice || t.Kind() == reflect.Array {
if err := json.Unmarshal(body, newVar); err != nil {
return err
}
got = reflect.ValueOf(newVar).Elem().Interface()
} else {
got = strings.TrimSpace(string(body))
}
if !reflect.DeepEqual(response, got) {
diff := cmp.Diff(response, got)
coloredDiff := formatDiff(diff)
return errors.New(fmt.Sprintf("response body does not match. \n "+
"expected: %v \n "+
"got : %v \n\n Diff: \n %v", truncate(response), truncate(got), coloredDiff))
}
return nil
}
}

// AssertPath asserts that the value at the given path in the response body satisfies the given assertion
// the path is a dot separated string representing the path to the value in the response body
// e.g. "data.user.0.name" will navigate to the first user in the data array and check if the name field satisfies the given assertion
func AssertPath[T comparable](path string, assert func(val T) error) AssertionFunc {
return func(body []byte) error {
var data any
if err := json.Unmarshal(body, &data); err != nil {
return err
}
if path[0] == '.' {
path = strings.Replace(path, ".", "", 1)
}
value, err := navigateJSON(data, path)
if err != nil {
return err
}
v, err := convertToType[T](value)
if err != nil {
return err
}
if err := assert(v); err != nil {
return err
}
return nil
}
}

func truncate(data interface{}) string {
value := fmt.Sprintf("%#v", data)
maxSize := bufio.MaxScanTokenSize - 100
if len(value) > maxSize {
value = value[0:maxSize] + "..."
}
return value
}
func formatDiff(diff string) string {
lines := strings.Split(diff, "\n")
var formattedDiff string
for _, line := range lines {
if strings.HasPrefix(line, "-") {
formattedDiff += fmt.Sprintf("\033[31m%s\033[0m\n", line)
} else if strings.HasPrefix(line, "+") {
formattedDiff += fmt.Sprintf("\033[32m%s\033[0m\n", line)
} else {
formattedDiff += fmt.Sprintf("%s\n", line)
}
}
return formattedDiff
}

func convertToType[T comparable](value interface{}) (T, error) {
var result T
targetType := reflect.TypeOf(result)
valueReflect := reflect.ValueOf(value)
if valueReflect.Type().ConvertibleTo(targetType) {
convertedValue := valueReflect.Convert(targetType)
result = convertedValue.Interface().(T)
return result, nil
}
if targetType.Kind() == reflect.Struct && valueReflect.Kind() == reflect.Map {
valueMap := valueReflect.Interface().(map[string]interface{})
structValue := reflect.ValueOf(&result).Elem()

for key, val := range valueMap {
field := structValue.FieldByName(key)
if field.IsValid() && field.CanSet() {
field.Set(reflect.ValueOf(val).Convert(field.Type()))
}
}
return result, nil
}
return result, fmt.Errorf("cannot convert value of type %s to type %s", valueReflect.Type(), targetType)
}
145 changes: 145 additions & 0 deletions assertions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package testpilot

import (
"errors"
"reflect"
"testing"
)

func TestAssertEqual(t *testing.T) {
type args struct {
response any
body []byte
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Test AssertEqual",
args: args{
response: map[string]interface{}{
"key": "value",
},
body: []byte(`{"key":"value"}`),
},
wantErr: false,
},
{
name: "Test Failure AssertEqual",
args: args{
response: map[string]interface{}{
"key": "value",
},
body: []byte(`{"key":"value1"}`),
},
wantErr: true,
},
{
name: "Test assert string",
args: args{
response: "value",
body: []byte(`value`),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := AssertEqual(tt.args.response)(tt.args.body)
if (err != nil) != tt.wantErr {
t.Errorf("AssertEqual() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestAssertPath(t *testing.T) {
type args[T comparable] struct {
path string
assert func(val T) error
body []byte
}
type testCase[T comparable] struct {
name string
args args[T]
wantErr bool
}
tests := []testCase[int]{
{
name: "Test AssertPath",
args: args[int]{
path: "key",
assert: func(val int) error {
if val != 1 {
return errors.New("value is not 1")
}
return nil
},
body: []byte(`{"key":1}`),
},
wantErr: false,
},
{
name: "Test Failure AssertPath",
args: args[int]{
path: "key",
assert: func(val int) error {
if val != 1 {
return errors.New("value is not 1")
}
return nil
},
body: []byte(`{"key":2}`),
},
wantErr: true,
},
{
name: "Test AssertPath with nested object",
args: args[int]{
path: "key.subkey",
assert: func(val int) error {
if val != 1 {
return errors.New("value is not 1")
}
return nil
},
body: []byte(`{"key":{"subkey":1}}`),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := AssertPath(tt.args.path, tt.args.assert)(tt.args.body)
if (err != nil) != tt.wantErr {
t.Errorf("AssertPath() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func Test_convertToType_String(t *testing.T) {
t.Run("Test convertToType string", func(t *testing.T) {
got, err := convertToType[string]("value")
if err != nil {
t.Errorf("convertToType() error = %v", err)
}
if got != "value" {
t.Errorf("convertToType() got = %v, want value", got)
}
})
t.Run("Test convertToType struct", func(t *testing.T) {
type testStruct struct {
Key string
}
got, err := convertToType[testStruct](map[string]any{"Key": "value"})
if err != nil {
t.Errorf("convertToType() error = %v", err)
}
if !reflect.DeepEqual(got, testStruct{"value"}) {
t.Errorf("convertToType() got = %v, want value", got)
}
})
}
18 changes: 18 additions & 0 deletions expect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package testpilot

type Expect struct {
expectedResponseCode *int
expectedResponseBody *AssertionFunc
}

// Status sets the expected response code
func (e *Expect) Status(code int) *Expect {
e.expectedResponseCode = &code
return e
}

// Body sets the expected response body
func (e *Expect) Body(assertionFunc AssertionFunc) *Expect {
e.expectedResponseBody = &assertionFunc
return e
}
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
Loading

0 comments on commit e8d3737

Please sign in to comment.