Skip to content
Merged

Dev #19

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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
*.so
*.dylib
bin
.qodo
cover.svg

# Test binary, built with `go test -c`
*.test
Expand Down
8 changes: 5 additions & 3 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
version: 2

snapshot:
version_template: "{{ incpatch .Version }}-next"

builds:
- main: ./cmd/qube
id: "qube"
binary: qube
ldflags:
- -s -w -X github.com/apiqube/cli/cmd/cli.Version={{.Version}}
- -s -w -X github.com/apiqube/cli/cmd/cli.Commit={{.ShortCommit}}
- -s -w -X github.com/apiqube/cli/cmd/cli.Date={{.Date}}
- -s -w -X github.com/apiqube/cli/cmd/cli.Version={{.Version}} -X github.com/apiqube/cli/cmd/cli.Commit={{.ShortCommit}} -X github.com/apiqube/cli/cmd/cli.Date={{.Date}}
env:
- CGO_ENABLED=0
goos: [linux, darwin, windows]
Expand Down
5 changes: 5 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ tasks:
cmds:
- go test -v -coverpkg=./... -coverprofile=cover.out ./...

cover:
desc: Create SVG cover heatmap from cover.out
cmds:
- go-cover-treemap -percent=true -w=1080 -h=360 -coverprofile cover.out > cover.svg

fmt:
desc: 🧹 Cleaning all go code
cmds:
Expand Down
4 changes: 2 additions & 2 deletions cmd/cli/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
"github.com/apiqube/cli/internal/core/store"
"github.com/apiqube/cli/internal/validate"
"github.com/apiqube/cli/ui/cli"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)

func init() {
Expand Down Expand Up @@ -124,5 +124,5 @@ func printPostApplySummary(mans []manifests.Manifest) {
}

func indentYAMLError(err *yaml.TypeError) string {
return " " + strings.Join(err.Errors, "\n ")
return fmt.Sprintf(" %s\n ", err.Error())
}
2 changes: 1 addition & 1 deletion cmd/cli/generator/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"github.com/apiqube/cli/internal/core/store"
"github.com/apiqube/cli/internal/operations"
"github.com/apiqube/cli/ui/cli"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)

var Cmd = &cobra.Command{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
version: v1
kind: HttpTest
metadata:
name: simple-test-example-1
namespace: simple-http-tests-1
name: test-example
namespace: complex-http-tests

spec:
target: http://127.0.0.1:8081
Expand All @@ -23,4 +23,11 @@ spec:
age: "{{ Fake.uint.10.100 }}"
address:
street: "{{ Fake.address }}"
number: "{{ Regex(\"^[a-z]{5,10}@[a-z]{5,10}\\.(com|net|org)$\") }}"
number: "{{ Regex(\"^[a-z]{5,10}@[a-z]{5,10}\\.(com|net|org)$\") }}"
save:
request:
body:
users: "*"
response:
body:
data: "*"
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ require (
github.com/charmbracelet/lipgloss v1.1.0
github.com/dgraph-io/badger/v4 v4.7.0
github.com/go-playground/validator/v10 v10.26.0
github.com/goccy/go-json v0.10.5
github.com/goccy/go-yaml v1.18.0
github.com/google/uuid v1.6.0
github.com/pterm/pterm v0.12.80
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
github.com/tidwall/gjson v1.18.0
golang.org/x/text v0.23.0
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand Down Expand Up @@ -98,4 +99,5 @@ require (
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
Expand Down
5 changes: 3 additions & 2 deletions internal/core/io/write.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package io

import (
"encoding/json"
"fmt"
"os"
"path/filepath"

"github.com/goccy/go-json"

"github.com/apiqube/cli/internal/core/manifests"
"github.com/apiqube/cli/internal/operations"
"gopkg.in/yaml.v3"
"github.com/goccy/go-yaml"
)

func WriteCombined(path string, format operations.ParseFormat, mans ...manifests.Manifest) error {
Expand Down
130 changes: 68 additions & 62 deletions internal/core/runner/assert/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ package assert

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"strings"

"github.com/apiqube/cli/internal/core/runner/templates"
"github.com/goccy/go-json"

"github.com/apiqube/cli/internal/core/manifests/kinds/tests"
"github.com/apiqube/cli/internal/core/runner/interfaces"
"github.com/apiqube/cli/internal/core/runner/templates"
)

type Type string
Expand All @@ -37,115 +37,108 @@ func NewRunner() *Runner {
}
}

// Assert runs all assertions and aggregates errors.
func (r *Runner) Assert(ctx interfaces.ExecutionContext, asserts []*tests.Assert, resp *http.Response, body []byte) error {
var err error

for _, assert := range asserts {
switch assert.Target {
for _, a := range asserts {
switch a.Target {
case Status.String():
err = errors.Join(err, r.assertStatus(ctx, assert, resp))
err = errors.Join(err, r.assertStatus(ctx, a, resp))
case Body.String():
err = errors.Join(err, r.assertBody(ctx, assert, resp, body))
err = errors.Join(err, r.assertBody(ctx, a, resp, body))
case Headers.String():
err = errors.Join(err, r.assertHeaders(ctx, assert, resp))
err = errors.Join(err, r.assertHeaders(ctx, a, resp))
default:
return fmt.Errorf("assert failed: unknown assert target %s", assert.Target)
return fmt.Errorf("assert failed: unknown assert target %s", a.Target)
}
}

return err
}

func (r *Runner) assertStatus(_ interfaces.ExecutionContext, assert *tests.Assert, resp *http.Response) error {
if assert.Equals != nil {
expectedCode, ok := assert.Equals.(int)
if !ok {
return fmt.Errorf("expected status type [int] got %T", assert.Equals)
func (r *Runner) assertStatus(_ interfaces.ExecutionContext, a *tests.Assert, resp *http.Response) error {
if a.Equals != nil {
expectedCode, err := toInt(a.Equals)
if err != nil {
return fmt.Errorf("expected status type [int] got %T", a.Equals)
}

if resp.StatusCode != expectedCode {
return fmt.Errorf("expected status code %v, got %v", expectedCode, resp.StatusCode)
}
}

if assert.Contains != "" {
if !strings.Contains(resp.Status, assert.Contains) {
return fmt.Errorf("expected %v to contain %q", resp.Status, assert.Contains)
if a.Contains != "" {
if !strings.Contains(resp.Status, a.Contains) {
return fmt.Errorf("expected %v to contain %q", resp.Status, a.Contains)
}
}

return nil
}

func (r *Runner) assertBody(_ interfaces.ExecutionContext, assert *tests.Assert, _ *http.Response, body []byte) error {
if assert.Exists {
func (r *Runner) assertBody(_ interfaces.ExecutionContext, a *tests.Assert, _ *http.Response, body []byte) error {
if a.Exists {
if len(body) == 0 {
return fmt.Errorf("expected not null body")
}

return nil
}

if assert.Template != "" {
tplResult, err := r.templateEngine.Execute(assert.Template)
if a.Template != "" {
tplResult, err := r.templateEngine.Execute(a.Template)
if err != nil {
return fmt.Errorf("template execution error: %v", err)
}

expected, err := json.Marshal(tplResult)
if err != nil {
return fmt.Errorf("template marshal error: %v", err)
}

if !reflect.DeepEqual(body, expected) {
if !bytes.Equal(body, expected) {
return fmt.Errorf("body doesn't match template\nexpected: %s\nactual: %s", expected, body)
}

return nil
}

if assert.Equals != nil {
if a.Equals != nil {
var expected any
if err := json.Unmarshal([]byte(fmt.Sprintf("%v", assert.Equals)), &expected); err != nil {
return fmt.Errorf("invalid Equals in body target value: %v", err)
// Try to unmarshal as JSON, fallback to string compare
if err := json.Unmarshal([]byte(fmt.Sprintf("%v", a.Equals)), &expected); err == nil {
var actual any
if marshalErr := json.Unmarshal(body, &actual); marshalErr == nil {
if !reflect.DeepEqual(actual, expected) {
return fmt.Errorf("expected body %v to equal %v", string(body), expected)
}
return nil
}
}

if !reflect.DeepEqual(body, expected) {
return fmt.Errorf("expected body %v to equal %v", string(body), expected)
// Fallback: compare as string
if string(body) != fmt.Sprint(a.Equals) {
return fmt.Errorf("expected body %v to equal %v", string(body), a.Equals)
}
return nil
}

if assert.Contains != "" {
if !bytes.Contains(body, []byte(assert.Contains)) {
return fmt.Errorf("expected '%v' in body", assert.Contains)
if a.Contains != "" {
if !bytes.Contains(body, []byte(a.Contains)) {
return fmt.Errorf("expected '%v' in body", a.Contains)
}

return nil
}

return nil
}

func (r *Runner) assertHeaders(_ interfaces.ExecutionContext, assert *tests.Assert, resp *http.Response) error {
if assert.Equals != nil {
if equals, ok := assert.Equals.(map[string]any); ok {
return fmt.Errorf("expected map type assertion got %T", assert.Equals)
} else {
for key, expectedVal := range equals {
actualVal := resp.Header.Get(key)
if fmt.Sprintf("%v", expectedVal) != actualVal {
return fmt.Errorf("expected header value %v, got %v", expectedVal, actualVal)
}
func (r *Runner) assertHeaders(_ interfaces.ExecutionContext, a *tests.Assert, resp *http.Response) error {
if a.Equals != nil {
equals, ok := a.Equals.(map[string]any)
if !ok {
return fmt.Errorf("expected map[string]any for header equals, got %T", a.Equals)
}
for key, expectedVal := range equals {
actualVal := resp.Header.Get(key)
if fmt.Sprintf("%v", expectedVal) != actualVal {
return fmt.Errorf("expected header %v value %v, got %v", key, expectedVal, actualVal)
}
}
}

if assert.Contains != "" {
if a.Contains != "" {
found := false
for _, values := range resp.Header {
for _, val := range values {
if strings.Contains(val, assert.Contains) {
if strings.Contains(val, a.Contains) {
found = true
break
}
Expand All @@ -154,17 +147,30 @@ func (r *Runner) assertHeaders(_ interfaces.ExecutionContext, assert *tests.Asse
break
}
}

if !found {
return fmt.Errorf("expected header contains %v but not found", assert.Contains)
return fmt.Errorf("expected header contains %v but not found", a.Contains)
}
}

if assert.Exists {
if a.Exists {
if len(resp.Header) == 0 {
return fmt.Errorf("expected some headers in response")
}
}

return nil
}

// toInt tries to convert interface{} to int for status code assertions.
func toInt(val any) (int, error) {
switch v := val.(type) {
case uint:
return int(v), nil
case uint64:
return int(v), nil
case int64:
return int(v), nil
case int:
return v, nil
default:
return 0, fmt.Errorf("cannot convert %T to int", val)
}
}
Loading
Loading