Skip to content
Merged

Dev #15

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: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: '3'
vars:
BINARY_NAME: qube
BUILD_DIR: C:/Users/admin/go/bin
MAIN: ./cmd
MAIN: ./cmd/qube
VERSION:
sh: git describe --tags --abbrev=0 2>/dev/null || echo "dev"

Expand Down
3 changes: 3 additions & 0 deletions cmd/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"os/signal"
"syscall"

"github.com/apiqube/cli/cmd/cli/run"

"github.com/apiqube/cli/cmd/cli/apply"
"github.com/apiqube/cli/cmd/cli/check"
"github.com/apiqube/cli/cmd/cli/cleanup"
Expand Down Expand Up @@ -40,6 +42,7 @@ var rootCmd = &cobra.Command{
func Execute() {
rootCmd.AddCommand(
versionCmd,
run.Cmd,
apply.Cmd,
check.Cmd,
cleanup.Cmd,
Expand Down
209 changes: 209 additions & 0 deletions cmd/cli/run/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package run

import (
"fmt"
"strings"

"github.com/apiqube/cli/internal/core/io"
"github.com/apiqube/cli/internal/core/manifests"
"github.com/apiqube/cli/internal/core/runner/context"
"github.com/apiqube/cli/internal/core/runner/executor"
"github.com/apiqube/cli/internal/core/runner/hooks"
runner "github.com/apiqube/cli/internal/core/runner/plan"
"github.com/apiqube/cli/internal/core/store"
"github.com/apiqube/cli/ui/cli"
"github.com/spf13/cobra"
)

var Cmd = &cobra.Command{
Use: "run",
Short: "Run tests by plan or generate it and run",
SilenceErrors: true,
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
opts, err := parseOptions(cmd)
if err != nil {
cli.Errorf("Failed to parse provided values: %v", err)
return
}

cli.Info("Loading manifests...")
loadedManifests, err := loadManifests(opts)
if err != nil {
cli.Errorf("Failed to load manifests: %v", err)
return
}

cli.Infof("Loaded %d manifests", len(loadedManifests))
cli.Info("Generating plan...")

manager := runner.NewPlanManagerBuilder().
WithManifests(loadedManifests...).Build()

planManifest, err := manager.Generate()
if err != nil {
cli.Errorf("Failed to generate plan: %v", err)
return
}

cli.Successf("Plan successfully generated")

ctxBuilder := context.NewCtxBuilder().
WithContext(cmd.Context()).
WithManifests(loadedManifests...)

registry := executor.NewDefaultExecutorRegistry()
hooksRunner := hooks.NewDefaultHooksRunner()

planRunner := executor.NewDefaultPlanRunner(registry, hooksRunner)

runCtx := ctxBuilder.Build()

if err = planRunner.RunPlan(runCtx, planManifest); err != nil {
return
}

cli.Successf("Plan successfully runned")
},
}

func init() {
Cmd.Flags().StringArrayP("names", "n", []string{}, "Names of manifests to generate (comma separated)")
Cmd.Flags().StringP("namespace", "s", "", "Namespace of manifests to generate")
Cmd.Flags().StringArrayP("ids", "i", []string{}, "IDs of manifests to generate (comma separated)")
Cmd.Flags().StringArrayP("hashes", "H", []string{}, "Hash prefixes for manifests (min 5 chars each)")

Cmd.Flags().StringP("file", "f", ".", "Path to manifest directory (default: current)")

Cmd.Flags().BoolP("output", "o", false, "Make output after generating")
Cmd.Flags().String("output-path", "", "Output path to save the plan (default: current directory)")
Cmd.Flags().String("output-format", "yaml", "Output format (yaml|json)")
}

type options struct {
names []string
namespace string
ids []string
hashes []string

file string

output bool
outputPath string
outputFormat string

flagsSet map[string]bool
}

func parseOptions(cmd *cobra.Command) (*options, error) {
opts := &options{
flagsSet: make(map[string]bool),
}

markFlag := func(name string) bool {
if cmd.Flags().Changed(name) {
opts.flagsSet[name] = true
return true
}
return false
}

if markFlag("names") {
opts.names, _ = cmd.Flags().GetStringArray("names")
}
if markFlag("namespace") {
opts.namespace, _ = cmd.Flags().GetString("namespace")
}
if markFlag("ids") {
opts.ids, _ = cmd.Flags().GetStringArray("ids")
}
if markFlag("hashes") {
opts.hashes, _ = cmd.Flags().GetStringArray("hashes")
}

if markFlag("file") {
opts.file, _ = cmd.Flags().GetString("file")
}

if markFlag("output") {
opts.output, _ = cmd.Flags().GetBool("output")
}
if markFlag("output-path") {
opts.outputPath, _ = cmd.Flags().GetString("output-path")
}
if markFlag("output-format") {
opts.outputFormat, _ = cmd.Flags().GetString("output-format")
}

exclusiveFlags := []string{"names", "namespace", "ids", "hashes", "file"}

var usedFlags []string
for _, flag := range exclusiveFlags {
if opts.flagsSet[flag] {
usedFlags = append(usedFlags, "--"+flag)
}
}

if len(usedFlags) > 1 {
return nil, fmt.Errorf(
"conflicting filters: %s\n"+
"these filters cannot be used together, please use only one",
strings.Join(usedFlags, " and "),
)
}

if err := validateOptions(opts); err != nil {
return nil, err
}

return opts, nil
}

func validateOptions(opts *options) error {
if !opts.flagsSet["names"] &&
!opts.flagsSet["namespace"] &&
!opts.flagsSet["ids"] &&
!opts.flagsSet["hashes"] &&
!opts.flagsSet["file"] {
return fmt.Errorf("at least one generate filter must be specified")
}
return nil
}

func loadManifests(opts *options) ([]manifests.Manifest, error) {
switch {
case opts.flagsSet["ids"]:
return store.Load(store.LoadOptions{
IDs: opts.ids,
})

case opts.flagsSet["file"]:
loadedMans, cachedMans, err := io.LoadManifests(opts.file)
if err == nil {
cli.Infof("Manifests from provided path %s loaded", opts.file)
}

loadedMans = append(loadedMans, cachedMans...)
return loadedMans, err

default:
query := store.NewQuery()
if opts.flagsSet["names"] {
for _, name := range opts.names {
query.WithExactName(name)
}
}

if opts.flagsSet["hashes"] {
for _, hash := range opts.hashes {
query.WithHashPrefix(hash)
}
}

if opts.flagsSet["namespace"] {
query.WithNamespace(opts.namespace)
}

return store.Search(query)
}
}
File renamed without changes.
75 changes: 75 additions & 0 deletions examples/simple-http-tests/http_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# API Test Configuration File
# --------------------------
# version: Defines the schema version for future compatibility
version: v1

# kind: Specifies the test type (HttpTest for HTTP API testing)
kind: HttpTest

# Metadata section - Contains identifying information
metadata:
# name: Unique identifier for this test suite
name: simple-test-example

# namespace: Logical grouping for organizational purposes
namespace: simple-http-tests

# spec: Main configuration container
spec:
# target: Base URL for all test cases (can be overridden per-case)
target: http://127.0.0.1:8081

# cases: List of test scenarios to execute
cases:
# Test Case 1: Basic GET request validation
- name: Get All Users Test # Descriptive test name
method: GET # HTTP method (GET/POST/PUT/etc)
endpoint: /users # Appended to target URL

# assert: Validation rules
assert:
- target: status # What to validate (status code)
equals: 200 # Expected value (HTTP 200 OK)

# Test Case 2: POST request with payload
- name: Create New User With Body
method: POST
endpoint: /users

# headers: Request headers to include
headers:
Content-Type: application/json # Specifies JSON payload

# body: Request payload (automatically JSON-encoded)
body:
name: "{{ Fake.name }}" # Generate fake name for request
email: "{{ Fake.email }}" # Generate fake email for request
age: "{{ Fake.uint.10.100 }}" # Generate fake positive number between 10 and 100 including
address:
street: "{{ Fake.email }}"
number: "{{ Fake.name }}"
assert:
- target: status
equals: 201 # HTTP 201 Created

# Test Case 3: Getting and validating a user
- name: Get User By ID Test
method: GET
endpoint: /users/1 # Endpoint with user ID
assert:
- target: status
equals: 200

# Test Case 4: Absolute URL test
- name: Always Fail Endpoint Test
method: GET
url: http://127.0.0.1:8081/fail # Overrides spec.target
assert:
- target: status
equals: 500 # Expecting server error

# Test Case 5: Performance testing
- name: Slow Endpoint Response Test
method: GET
endpoint: /slow?delay=2s # Test endpoint with artificial delay
timeout: 3s # Fail if response > 3 seconds
38 changes: 0 additions & 38 deletions examples/simple/http_test.yaml

This file was deleted.

File renamed without changes.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.24.3
require (
github.com/adrg/xdg v0.5.3
github.com/blevesearch/bleve/v2 v2.5.1
github.com/brianvoe/gofakeit/v7 v7.2.1
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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFx
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.2.3 h1:7Y0r+a3diEvlazsncexq1qoFOcBd64xwMS7aDm4lo1s=
github.com/blevesearch/zapx/v16 v16.2.3/go.mod h1:wVJ+GtURAaRG9KQAMNYyklq0egV+XJlGcXNCE0OFjjA=
github.com/brianvoe/gofakeit/v7 v7.2.1 h1:AGojgaaCdgq4Adzrd2uWdbGNDyX6MWNhHdQBraNfOHI=
github.com/brianvoe/gofakeit/v7 v7.2.1/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
Expand Down
2 changes: 1 addition & 1 deletion internal/core/manifests/kinds/tests/api/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type Http struct {

Spec struct {
Target string `yaml:"target,omitempty" json:"target,omitempty" validate:"required"`
Cases []HttpCase `yaml:"cases" json:"cases" valid:"required,min=1,max=100,dive"`
Cases []HttpCase `yaml:"cases" json:"cases" validate:"required,min=1,max=100,dive"`
} `yaml:"spec" json:"spec" validate:"required"`

kinds.Dependencies `yaml:",inline" json:",inline" validate:"omitempty"`
Expand Down
Loading
Loading