mirror of https://github.com/gabehf/sp9rk.git
commit
c05e6d11e8
@ -0,0 +1,102 @@
|
|||||||
|
This project is under active development and may have breaking API changes! Use at your own risk.
|
||||||
|
# Sp9rk - Easily save and call web requests
|
||||||
|
Pronounced "spark".
|
||||||
|
|
||||||
|
Sp9rk allows you to easily register and execute complex web requests without having to rewrite long cUrl commands. Just create the request once and execute as many times as you need.
|
||||||
|
## Install
|
||||||
|
### From Source
|
||||||
|
Ensure you have go version 1.22.2 or greater.
|
||||||
|
|
||||||
|
Clone the repo
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/gabehf/sp9rk
|
||||||
|
cd sp9rk
|
||||||
|
```
|
||||||
|
Run `go build`
|
||||||
|
```bash
|
||||||
|
go build
|
||||||
|
```
|
||||||
|
Do whatever you want with the executable
|
||||||
|
```bash
|
||||||
|
$ sp9rk -v
|
||||||
|
sp9rk version v0.0.1
|
||||||
|
```
|
||||||
|
## Usage
|
||||||
|
### Create
|
||||||
|
Register an application
|
||||||
|
```bash
|
||||||
|
$ sp9rk create app -u http://localhost:8080 ExampleApp
|
||||||
|
Created application ExampleApp
|
||||||
|
```
|
||||||
|
Register a request for the app
|
||||||
|
```bash
|
||||||
|
$ sp9rk create req \
|
||||||
|
-a ExampleApp \
|
||||||
|
-p "/path" \
|
||||||
|
-b "{'body':'request body'}" \
|
||||||
|
-X POST \
|
||||||
|
MyRequest
|
||||||
|
Created request MyRequest
|
||||||
|
```
|
||||||
|
## Switch
|
||||||
|
You can set the default application your commands effect using `switch`
|
||||||
|
```bash
|
||||||
|
$ sp9rk switch ExampleApp
|
||||||
|
ExampleApp
|
||||||
|
```
|
||||||
|
Now sp9rk will default to using that application, instead of needing to specify the app with the `-a` flag
|
||||||
|
## Call
|
||||||
|
Call the request
|
||||||
|
```bash
|
||||||
|
$ sp9rk call MyRequest
|
||||||
|
Hello, World!
|
||||||
|
```
|
||||||
|
You can also return more information with the `--verbose -v` flag when making a call
|
||||||
|
```bash
|
||||||
|
$ sp9rk call -v MyRequest
|
||||||
|
Request: GET http://localhost:8080/path
|
||||||
|
RequestBody: "{'body':'request body'}"
|
||||||
|
Headers: {}
|
||||||
|
Latency: 103.731894ms
|
||||||
|
Status: 200 OK
|
||||||
|
ResponseBody: Hello, World!
|
||||||
|
```
|
||||||
|
## Edit
|
||||||
|
You can edit the definitions of existing requests or apps
|
||||||
|
```bash
|
||||||
|
$ sp9rk edit req --method POST --path /other-path MyRequest
|
||||||
|
|
||||||
|
$ sp9rk call -v MyRequest
|
||||||
|
Request: POST http://localhost:8080/other-path
|
||||||
|
RequestBody: "{'body':'request body'}"
|
||||||
|
Headers: {}
|
||||||
|
Latency: 67.82371ms
|
||||||
|
Status: 200 OK
|
||||||
|
ResponseBody: Hello again, World!
|
||||||
|
```
|
||||||
|
## Delete
|
||||||
|
You can delete apps or requests using `delete app/req`
|
||||||
|
```bash
|
||||||
|
$ sp9rk delete req MyRequest
|
||||||
|
You are about to delete the request MyRequest in application ExampleApp.
|
||||||
|
This action cannot be undone.
|
||||||
|
Are you sure? [y/N]: y
|
||||||
|
request MyRequest has been deleted
|
||||||
|
```
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
- [ ] Allow users to specify the number of redirects to follow before stopping
|
||||||
|
- [ ] Allow flags to be saved along with requests
|
||||||
|
- [ ] Allow parameters to be used inside both requests paths and bodies
|
||||||
|
- [ ] Allow for requests to use files as request bodies
|
||||||
|
- [ ] Allow for commands flags to be used both before and after arguments (i.e. allowing `sp9rk create req MyReq -a MyApp` as well as `sp9rk create -a MyApp MyReq`)
|
||||||
|
- [ ] Add easy install script and/or package
|
||||||
|
|
||||||
|
# Albums that fueled development
|
||||||
|
| Album | Artist |
|
||||||
|
|-----------------------------|------------------|
|
||||||
|
| Emotion | Carly Rae Jepsen |
|
||||||
|
| To Pimp a Butterfly | Kendrick Lamar |
|
||||||
|
| good kid, m.A.A.d city | Kendrick Lamar |
|
||||||
|
| if i could make it go quiet | girl in red |
|
||||||
|
| DROP | Minami (美波) |
|
||||||
@ -0,0 +1,532 @@
|
|||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateApplication(cfgPath string) func(ctx *cli.Context) error {
|
||||||
|
return func(ctx *cli.Context) error {
|
||||||
|
if ctx.NArg() != 1 {
|
||||||
|
return errors.New("create app must have exactly one argument")
|
||||||
|
}
|
||||||
|
if !valid(ctx.Args().Get(0)) {
|
||||||
|
return errors.New("application name must only contain letters, numbers, dashes and underscores")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(AppPath(cfgPath, ctx.Args().Get(0))); err == nil {
|
||||||
|
return errors.New("application already exists")
|
||||||
|
}
|
||||||
|
err := WriteAppFiles(cfgPath, &AppInfo{
|
||||||
|
Version: "1",
|
||||||
|
Name: ctx.Args().Get(0),
|
||||||
|
Description: ctx.String("description"),
|
||||||
|
Host: ctx.String("host"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Created application %s\n", ctx.Args().Get(0))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateRequest(cfgPath string) func(ctx *cli.Context) error {
|
||||||
|
return func(ctx *cli.Context) error {
|
||||||
|
if ctx.NArg() < 1 || ctx.NArg() > 1 {
|
||||||
|
return errors.New("create req must have exactly one argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
app, err := getApp(cfgPath, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqName := ctx.Args().Get(0)
|
||||||
|
|
||||||
|
if !valid(app) || !appExists(cfgPath, app) {
|
||||||
|
return errors.New("application does not exist")
|
||||||
|
}
|
||||||
|
if !valid(reqName) {
|
||||||
|
return errors.New("request name must only contain letters, numbers, dashes and underscores")
|
||||||
|
}
|
||||||
|
|
||||||
|
path := ReqPath(cfgPath, app, reqName)
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return errors.New("request already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := WriteRequestFiles(cfgPath, app, &RequestInfo{
|
||||||
|
Version: "1",
|
||||||
|
Name: reqName,
|
||||||
|
Description: ctx.String("description"),
|
||||||
|
Method: ctx.String("method"),
|
||||||
|
Path: ctx.String("path"),
|
||||||
|
Headers: ctx.StringSlice("header"),
|
||||||
|
Body: ctx.String("body"),
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Created request %s\n", reqName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func EditApplication(cfgPath string) func(ctx *cli.Context) error {
|
||||||
|
return func(ctx *cli.Context) error {
|
||||||
|
if ctx.NArg() != 1 {
|
||||||
|
return errors.New("edit app must have exactly one argument")
|
||||||
|
}
|
||||||
|
if !valid(ctx.Args().Get(0)) {
|
||||||
|
return errors.New("application name is invalid")
|
||||||
|
}
|
||||||
|
contents, err := os.ReadFile(AppInfoFilePath(cfgPath, ctx.Args().Get(0)))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("application does not exist")
|
||||||
|
}
|
||||||
|
appinfo := new(AppInfo)
|
||||||
|
err = yaml.Unmarshal(contents, appinfo)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to read application info")
|
||||||
|
}
|
||||||
|
if ctx.String("description") != "" {
|
||||||
|
appinfo.Description = ctx.String("description")
|
||||||
|
}
|
||||||
|
if ctx.String("host") != "" {
|
||||||
|
appinfo.Host = ctx.String("host")
|
||||||
|
}
|
||||||
|
err = WriteAppFiles(cfgPath, appinfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func EditRequest(cfgPath string) func(ctx *cli.Context) error {
|
||||||
|
return func(ctx *cli.Context) error {
|
||||||
|
if ctx.NArg() < 1 || ctx.NArg() > 1 {
|
||||||
|
return errors.New("create req must have exactly one argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
app, err := getApp(cfgPath, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqName := ctx.Args().Get(0)
|
||||||
|
|
||||||
|
if !valid(app) || !appExists(cfgPath, app) {
|
||||||
|
return errors.New("application does not exist")
|
||||||
|
}
|
||||||
|
if !valid(reqName) {
|
||||||
|
return errors.New("request name is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
path := ReqPath(cfgPath, app, reqName)
|
||||||
|
contents, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("request does not exists")
|
||||||
|
}
|
||||||
|
reqinfo := new(RequestInfo)
|
||||||
|
err = yaml.Unmarshal(contents, reqinfo)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("request info is malformed or corrupted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.String("description") != "" {
|
||||||
|
reqinfo.Description = ctx.String("description")
|
||||||
|
}
|
||||||
|
if ctx.String("method") != "" {
|
||||||
|
reqinfo.Method = ctx.String("method")
|
||||||
|
}
|
||||||
|
if ctx.String("path") != "" {
|
||||||
|
reqinfo.Path = ctx.String("path")
|
||||||
|
}
|
||||||
|
if ctx.String("body") != "" {
|
||||||
|
reqinfo.Body = ctx.String("body")
|
||||||
|
}
|
||||||
|
if ctx.StringSlice("header") != nil {
|
||||||
|
reqinfo.Headers = ctx.StringSlice("header")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := WriteRequestFiles(cfgPath, app, reqinfo); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteApplication(cfgPath string) func(ctx *cli.Context) error {
|
||||||
|
return func(ctx *cli.Context) error {
|
||||||
|
if ctx.NArg() < 1 || ctx.NArg() > 1 {
|
||||||
|
return errors.New("delete app must have exactly one argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
app := ctx.Args().Get(0)
|
||||||
|
|
||||||
|
if !valid(app) {
|
||||||
|
return errors.New("application name is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
_path := AppPath(cfgPath, app)
|
||||||
|
|
||||||
|
files, err := os.ReadDir(_path)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("application does not exist")
|
||||||
|
}
|
||||||
|
fmt.Printf(
|
||||||
|
"You are about to delete the application %s and %d associated request(s).\nThis action cannot be undone.\n",
|
||||||
|
app,
|
||||||
|
len(files)-1, // -1 because .appinfo isnt a req
|
||||||
|
)
|
||||||
|
if ctx.Bool("confirm") || ConfirmPrompt() {
|
||||||
|
err := os.RemoveAll(_path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Print("application " + app + " has been deleted")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Println("delete aborted")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func DeleteRequest(cfgPath string) func(ctx *cli.Context) error {
|
||||||
|
return func(ctx *cli.Context) error {
|
||||||
|
|
||||||
|
if ctx.NArg() < 1 || ctx.NArg() > 1 {
|
||||||
|
return errors.New("delete request must have exactly one argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
app, err := getApp(cfgPath, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqName := ctx.Args().Get(0)
|
||||||
|
|
||||||
|
if !valid(reqName) {
|
||||||
|
return errors.New("request name is invalid")
|
||||||
|
}
|
||||||
|
if !valid(app) || !appExists(cfgPath, app) {
|
||||||
|
return errors.New("application does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
_path := ReqPath(cfgPath, app, reqName)
|
||||||
|
|
||||||
|
if _, err := os.Stat(_path); err != nil {
|
||||||
|
return errors.New("request does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(
|
||||||
|
"You are about to delete the request %s in application %s.\nThis action cannot be undone.\n",
|
||||||
|
reqName,
|
||||||
|
app,
|
||||||
|
)
|
||||||
|
if ctx.Bool("confirm") || ConfirmPrompt() {
|
||||||
|
err := os.Remove(_path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Print("request " + reqName + " has been deleted")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Println("delete aborted")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Switch(cfgPath string) func(ctx *cli.Context) error {
|
||||||
|
return func(ctx *cli.Context) error {
|
||||||
|
if ctx.NArg() < 1 {
|
||||||
|
return errors.New("expected argument")
|
||||||
|
}
|
||||||
|
if ctx.NArg() > 1 {
|
||||||
|
return errors.New("expected exactly one argument")
|
||||||
|
}
|
||||||
|
app := ctx.Args().Get(0)
|
||||||
|
if !valid(app) {
|
||||||
|
return errors.New("application does not exist")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(AppPath(cfgPath, app)); err != nil {
|
||||||
|
return errors.New("application does not exist")
|
||||||
|
}
|
||||||
|
err := os.WriteFile(path.Join(cfgPath, "current_app"), []byte(ctx.Args().First()), 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Print(app)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListApplications(cfgPath string) func(ctx *cli.Context) error {
|
||||||
|
return func(ctx *cli.Context) error {
|
||||||
|
_path := AppPath(cfgPath, "")
|
||||||
|
dirs, err := os.ReadDir(_path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var apps string
|
||||||
|
for _, dir := range dirs {
|
||||||
|
appinfo := new(AppInfo)
|
||||||
|
contents, err := os.ReadFile(AppInfoFilePath(cfgPath, dir.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to read application info")
|
||||||
|
}
|
||||||
|
err = yaml.Unmarshal(contents, appinfo)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(".appinfo is malformed or corrupted")
|
||||||
|
}
|
||||||
|
if appinfo.Description == "" {
|
||||||
|
apps += appinfo.Name + "\n"
|
||||||
|
} else {
|
||||||
|
apps += appinfo.Name + ": " + appinfo.Description + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Print(apps)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListRequests(cfgPath string) func(ctx *cli.Context) error {
|
||||||
|
return func(ctx *cli.Context) error {
|
||||||
|
app, err := getApp(cfgPath, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_path := AppPath(cfgPath, app)
|
||||||
|
files, err := os.ReadDir(_path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reqs := ""
|
||||||
|
for _, file := range files {
|
||||||
|
if file.Name() == ".appinfo" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
reqinfo := new(RequestInfo)
|
||||||
|
contents, err := os.ReadFile(path.Join(AppPath(cfgPath, app), file.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to read request info")
|
||||||
|
}
|
||||||
|
err = yaml.Unmarshal(contents, reqinfo)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("request file is malformed or corrupted")
|
||||||
|
}
|
||||||
|
if reqinfo.Description == "" {
|
||||||
|
reqs += reqinfo.Name + "\n"
|
||||||
|
} else {
|
||||||
|
reqs += reqinfo.Name + ": " + reqinfo.Description + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Print(reqs)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func ListAll(cfgPath string) func(ctx *cli.Context) error {
|
||||||
|
return func(ctx *cli.Context) error {
|
||||||
|
apps, err := os.ReadDir(AppPath(cfgPath, ""))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
output := ""
|
||||||
|
if len(apps) < 1 {
|
||||||
|
fmt.Println("No applications or requests")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, app := range apps {
|
||||||
|
reqfiles, err := os.ReadDir(AppPath(cfgPath, app.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(reqfiles) <= 1 {
|
||||||
|
output += app.Name() + "\n"
|
||||||
|
} else {
|
||||||
|
output += app.Name() + ":\n"
|
||||||
|
}
|
||||||
|
for _, reqfile := range reqfiles {
|
||||||
|
if reqfile.Name() == ".appinfo" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
reqinfo := new(RequestInfo)
|
||||||
|
contents, err := os.ReadFile(path.Join(AppPath(cfgPath, app.Name()), reqfile.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to read request info")
|
||||||
|
}
|
||||||
|
err = yaml.Unmarshal(contents, reqinfo)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("request file is malformed or corrupted")
|
||||||
|
}
|
||||||
|
if reqinfo.Description == "" {
|
||||||
|
output += "\t- " + reqinfo.Name + "\n"
|
||||||
|
} else {
|
||||||
|
output += "\t- " + reqinfo.Name + ": " + reqinfo.Description + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Print(output)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func InfoApplication(cfgPath string) func(ctx *cli.Context) error {
|
||||||
|
return func(ctx *cli.Context) error {
|
||||||
|
if ctx.NArg() < 1 {
|
||||||
|
return errors.New("expected argument")
|
||||||
|
}
|
||||||
|
if ctx.NArg() > 1 {
|
||||||
|
return errors.New("expected exactly one argument")
|
||||||
|
}
|
||||||
|
if !valid(ctx.Args().Get(0)) {
|
||||||
|
return errors.New("application name is invalid")
|
||||||
|
}
|
||||||
|
contents, err := os.ReadFile(AppInfoFilePath(cfgPath, ctx.Args().Get(0)))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to read app info")
|
||||||
|
}
|
||||||
|
appinfo := new(AppInfo)
|
||||||
|
err = yaml.Unmarshal(contents, appinfo)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to read app info")
|
||||||
|
}
|
||||||
|
fmt.Printf("%s:\n\tDescription: %s\n\tHost: %s\n", appinfo.Name, appinfo.Description, appinfo.Host)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func InfoRequest(cfgPath string) func(ctx *cli.Context) error {
|
||||||
|
return func(ctx *cli.Context) error {
|
||||||
|
if ctx.NArg() < 1 {
|
||||||
|
return errors.New("expected argument")
|
||||||
|
}
|
||||||
|
if ctx.NArg() > 1 {
|
||||||
|
return errors.New("expected exactly one argument")
|
||||||
|
}
|
||||||
|
app, err := getApp(cfgPath, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !valid(app) {
|
||||||
|
return errors.New("application name is invalid")
|
||||||
|
}
|
||||||
|
if !valid(ctx.Args().Get(0)) {
|
||||||
|
return errors.New("request name is invalid")
|
||||||
|
}
|
||||||
|
contents, err := os.ReadFile(ReqPath(cfgPath, app, ctx.Args().Get(0)))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to read request info")
|
||||||
|
}
|
||||||
|
fileLines := strings.Split(string(contents), "\n")
|
||||||
|
fmt.Println(ctx.Args().Get(0) + ":\n\t" + strings.Join(fileLines[2:len(fileLines)-1], "\n\t"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerboseCallResponse struct {
|
||||||
|
Request string `yaml:"Request"`
|
||||||
|
RequestBody string `yaml:"RequestBody"`
|
||||||
|
Headers map[string]string `yaml:"Headers"`
|
||||||
|
Latency string `yaml:"Latency"`
|
||||||
|
Status string `yaml:"Status"`
|
||||||
|
ResponseBody string `yaml:"ResponseBody"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Call(cfgPath string, httpClient http.Client) func(ctx *cli.Context) error {
|
||||||
|
return func(ctx *cli.Context) error {
|
||||||
|
if ctx.NArg() < 1 {
|
||||||
|
return errors.New("expected argument")
|
||||||
|
}
|
||||||
|
if ctx.NArg() > 1 {
|
||||||
|
return errors.New("expected exactly one argument")
|
||||||
|
}
|
||||||
|
app, err := getApp(cfgPath, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !valid(app) {
|
||||||
|
return errors.New("application name is invalid")
|
||||||
|
}
|
||||||
|
if !valid(ctx.Args().Get(0)) {
|
||||||
|
return errors.New("request name is invalid")
|
||||||
|
}
|
||||||
|
// retrieve app information
|
||||||
|
contents, err := os.ReadFile(AppInfoFilePath(cfgPath, app))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to retrieve app information")
|
||||||
|
}
|
||||||
|
appinfo := new(AppInfo)
|
||||||
|
err = yaml.Unmarshal(contents, appinfo)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("appinfo file is malformed or corrupted")
|
||||||
|
}
|
||||||
|
// retrieve request information
|
||||||
|
contents, err = os.ReadFile(ReqPath(cfgPath, app, ctx.Args().Get(0)))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to retrieve request information")
|
||||||
|
}
|
||||||
|
reqinfo := new(RequestInfo)
|
||||||
|
err = yaml.Unmarshal(contents, reqinfo)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("request file is malformed or corrupted")
|
||||||
|
}
|
||||||
|
body := bytes.NewBuffer([]byte(reqinfo.Body))
|
||||||
|
req, err := http.NewRequest(reqinfo.Method, appinfo.Host+reqinfo.Path, body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to create web request")
|
||||||
|
}
|
||||||
|
for _, header := range reqinfo.Headers {
|
||||||
|
h := strings.Split(header, ": ")
|
||||||
|
if len(h) < 2 {
|
||||||
|
return errors.New("malformed header(s)")
|
||||||
|
}
|
||||||
|
req.Header.Add(h[0], h[1])
|
||||||
|
}
|
||||||
|
if ctx.Bool("no-redirect") {
|
||||||
|
httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }
|
||||||
|
}
|
||||||
|
t1 := time.Now()
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
t2 := time.Now()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to send web request")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if ctx.Bool("fail") && resp.StatusCode >= 400 {
|
||||||
|
return errors.New("")
|
||||||
|
}
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to read response body")
|
||||||
|
}
|
||||||
|
if !ctx.Bool("verbose") {
|
||||||
|
fmt.Print(string(respBody) + "\n")
|
||||||
|
} else if req.Method == "GET" {
|
||||||
|
var out VerboseCallResponse
|
||||||
|
out.Request = req.Method + " " + req.URL.String()
|
||||||
|
out.RequestBody = reqinfo.Body
|
||||||
|
out.Headers = make(map[string]string)
|
||||||
|
for _, header := range reqinfo.Headers {
|
||||||
|
k := strings.Split(header, ": ")[0]
|
||||||
|
v := strings.Split(header, ": ")[1]
|
||||||
|
out.Headers[k] = v
|
||||||
|
}
|
||||||
|
out.Latency = fmt.Sprintf("%v", t2.Sub(t1))
|
||||||
|
out.Status = resp.Status
|
||||||
|
out.ResponseBody = string(respBody)
|
||||||
|
o, err := yaml.Marshal(out)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to generate command output")
|
||||||
|
}
|
||||||
|
fmt.Print(string(o))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,618 @@
|
|||||||
|
package action_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gabehf/sp9rk/action"
|
||||||
|
"github.com/gabehf/sp9rk/app"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pretty much all the other tests rely on this test working...
|
||||||
|
// i know its bad practice but I dont want to manually create every mock app and request files
|
||||||
|
// manually every time i make the test.
|
||||||
|
//
|
||||||
|
// Basically, if this test fails, fix it FIRST.
|
||||||
|
func TestWriteFiles(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionWriteFiles", ".sp9rk", "tests")
|
||||||
|
err := action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Version: "1",
|
||||||
|
Name: "TestApp",
|
||||||
|
Description: "test app",
|
||||||
|
Host: "localhost",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err, "FIX FIRST: failed to write app files")
|
||||||
|
p := action.AppPath(cfgPath, "TestApp")
|
||||||
|
_, err = os.Stat(p)
|
||||||
|
assert.NoError(t, err, "failed to create app directory")
|
||||||
|
_, err = os.Stat(path.Join(p, ".appinfo"))
|
||||||
|
assert.NoError(t, err, "failed to create .appinfo file")
|
||||||
|
err = action.WriteRequestFiles(cfgPath, "TestApp", &action.RequestInfo{
|
||||||
|
Version: "1",
|
||||||
|
Name: "MyReq",
|
||||||
|
Description: "my request",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err, "FIX FIRST: failed to write request file")
|
||||||
|
_, err = os.Stat(path.Join(p, "MyReq.yml"))
|
||||||
|
assert.NoError(t, err, "failed to stat request file")
|
||||||
|
os.RemoveAll("TestActionWriteFiles")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionCreateApplication(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionCreateApplication", ".sp9rk", "tests")
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
assert.Error(
|
||||||
|
t,
|
||||||
|
RunWithArgs(app, "create", "app"),
|
||||||
|
"create app should fail with no args",
|
||||||
|
)
|
||||||
|
assert.Error(
|
||||||
|
t,
|
||||||
|
RunWithArgs(app, "create", "app", "-u", "hostname123", "../malicious/path"),
|
||||||
|
"create app should fail invalid name",
|
||||||
|
)
|
||||||
|
assert.Error(
|
||||||
|
t,
|
||||||
|
RunWithArgs(app, "create", "app", "-u", "hostname123", "TestApp", "secondArgument"),
|
||||||
|
"create app should fail with >1 args",
|
||||||
|
)
|
||||||
|
assert.NoError(
|
||||||
|
t,
|
||||||
|
RunWithArgs(app, "create", "app", "-u", "hostname123", "TestApp"),
|
||||||
|
"create app should succeed with 1 args",
|
||||||
|
)
|
||||||
|
assert.Error(
|
||||||
|
t,
|
||||||
|
RunWithArgs(app, "create", "app", "-u", "hostname123", "TestApp"),
|
||||||
|
"create app should fail when app already exists",
|
||||||
|
)
|
||||||
|
// cleanup
|
||||||
|
os.RemoveAll("TestActionCreateApplication")
|
||||||
|
}
|
||||||
|
func TestActionCreateRequest(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionCreateRequest", ".sp9rk", "tests")
|
||||||
|
// simulate apps being created
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestApp",
|
||||||
|
})
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestAppTwo",
|
||||||
|
})
|
||||||
|
// tests
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
|
||||||
|
assert.Error(t, RunWithArgs(app, "create", "req"), "create req should fail with no args")
|
||||||
|
assert.Error(t, RunWithArgs(app, "create", "req", "TestReq"), "create req should fail with no default app")
|
||||||
|
os.WriteFile(path.Join(cfgPath, "current_app"), []byte("TestApp"), 0700)
|
||||||
|
assert.NoError(t, RunWithArgs(app, "create", "req", "TestReq"), "create req should succeed with default app")
|
||||||
|
assert.Error(t, RunWithArgs(app, "create", "req", "TestReq", "SecondArg"), "create req should fail with >1 args")
|
||||||
|
assert.NoError(t, RunWithArgs(app, "create", "req", "-a", "TestAppTwo", "TestReq"), "create req should succeed with specified app")
|
||||||
|
assert.Error(t, RunWithArgs(app, "create", "req", "-a", "TestAppThree", "TestReq"), "create req should fail with unknown app")
|
||||||
|
assert.Error(t, RunWithArgs(app, "create", "req", "../malicious/path"), "create req should fail invalid name")
|
||||||
|
assert.Error(t, RunWithArgs(app, "create", "req", "-a", "TestAppTwo", "TestReq"), "create req should fail if it already exists")
|
||||||
|
contents, _ := os.ReadFile(path.Join(action.AppPath(cfgPath, "TestApp"), "TestReq.yml"))
|
||||||
|
reqinfo := new(action.RequestInfo)
|
||||||
|
err := yaml.Unmarshal(contents, reqinfo)
|
||||||
|
assert.NoError(t, err, "contents should be YAML")
|
||||||
|
assert.EqualValues(t, reqinfo.Name, "TestReq")
|
||||||
|
assert.EqualValues(t, reqinfo.Description, "")
|
||||||
|
assert.EqualValues(t, reqinfo.Method, "GET")
|
||||||
|
assert.EqualValues(t, reqinfo.Path, "/")
|
||||||
|
assert.EqualValues(t, reqinfo.Body, "")
|
||||||
|
assert.Empty(t, reqinfo.Headers)
|
||||||
|
|
||||||
|
// flag tests
|
||||||
|
assert.NoError(
|
||||||
|
t,
|
||||||
|
RunWithArgs(app, "create", "req",
|
||||||
|
"-d", "description",
|
||||||
|
"-X", "POST",
|
||||||
|
"-p", "/path",
|
||||||
|
"-b", "request body",
|
||||||
|
"-H", "X-Header-One: 1",
|
||||||
|
"-H", "X-Header-Two: 2",
|
||||||
|
"TestReqTwo",
|
||||||
|
),
|
||||||
|
"create req should succeed with flags",
|
||||||
|
)
|
||||||
|
contents, _ = os.ReadFile(path.Join(action.AppPath(cfgPath, "TestApp"), "TestReqTwo.yml"))
|
||||||
|
reqinfo = new(action.RequestInfo)
|
||||||
|
err = yaml.Unmarshal(contents, reqinfo)
|
||||||
|
assert.NoError(t, err, "contents should be YAML")
|
||||||
|
if reqinfo.Headers == nil || len(reqinfo.Headers) < 2 {
|
||||||
|
assert.FailNow(t, "file contents were not parsed successfully")
|
||||||
|
}
|
||||||
|
assert.EqualValues(t, reqinfo.Name, "TestReqTwo")
|
||||||
|
assert.EqualValues(t, reqinfo.Description, "description")
|
||||||
|
assert.EqualValues(t, reqinfo.Method, "POST")
|
||||||
|
assert.EqualValues(t, reqinfo.Path, "/path")
|
||||||
|
assert.EqualValues(t, reqinfo.Body, "request body")
|
||||||
|
assert.EqualValues(t, reqinfo.Headers[0], "X-Header-One: 1")
|
||||||
|
assert.EqualValues(t, reqinfo.Headers[1], "X-Header-Two: 2")
|
||||||
|
// cleanup
|
||||||
|
os.RemoveAll("TestActionCreateRequest")
|
||||||
|
}
|
||||||
|
func TestActionSwitch(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionSwitch", ".sp9rk", "tests")
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
assert.Error(t, RunWithArgs(app, "switch"), "switch should fail with no args")
|
||||||
|
assert.Error(t, RunWithArgs(app, "switch", "TestApp"), "switch should fail with no apps existing")
|
||||||
|
// simulate apps being created and one set as default
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestApp",
|
||||||
|
})
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestAppTwo",
|
||||||
|
})
|
||||||
|
os.WriteFile(path.Join(cfgPath, "current_app"), []byte("TestApp"), 0700)
|
||||||
|
// tests
|
||||||
|
assert.Error(t, RunWithArgs(app, "switch"), "switch should fail with no args")
|
||||||
|
output, err := captureOutput(RunWithArgs, app, "switch", "TestApp")
|
||||||
|
assert.NoError(t, err, "switch should NOT fail with existing app")
|
||||||
|
assert.Equal(t, "TestApp", output, "switch output should be equal to new app name on success")
|
||||||
|
assert.Error(t, RunWithArgs(app, "switch", "../malicious/path"), "switch should fail invalid name")
|
||||||
|
assert.Error(t, RunWithArgs(app, "switch", "TestApp", "somethingElse"), "switch should fail with >1 arg")
|
||||||
|
assert.Error(t, RunWithArgs(app, "switch", "NotRealApp"), "switch should fail with unknown app")
|
||||||
|
os.RemoveAll("TestActionSwitch")
|
||||||
|
}
|
||||||
|
func TestActionDeleteApp(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionDeleteApp", ".sp9rk", "tests")
|
||||||
|
// simulate apps being created
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestApp",
|
||||||
|
Description: "this is a very cool app that does cool stuff",
|
||||||
|
Host: "http://localhost:3000",
|
||||||
|
})
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestAppTwo",
|
||||||
|
Description: "this is another very cool app that does cool stuff",
|
||||||
|
Host: "http://localhost:3001",
|
||||||
|
})
|
||||||
|
// tests
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
assert.Error(t, RunWithArgs(app, "delete", "app"), "delete app should fail with no args")
|
||||||
|
assert.NoError(t, RunWithArgs(app, "delete", "app", "--confirm", "TestApp"), "delete app should NOT fail with existing app")
|
||||||
|
assert.Error(t, RunWithArgs(app, "delete", "app", "../malicious/path"), "delete app should fail invalid name")
|
||||||
|
assert.Error(t, RunWithArgs(app, "delete", "app", "TestAppTwo", "somethingElse"), "delete app should fail with >1 arg")
|
||||||
|
assert.Error(t, RunWithArgs(app, "delete", "app", "NotRealApp"), "delete app should fail with unknown app")
|
||||||
|
assert.NoError(t, RunWithArgs(app, "delete", "app", "--confirm", "TestAppTwo"), "delete app should NOT fail with existing app")
|
||||||
|
assert.Error(t, RunWithArgs(app, "delete", "app", "TestAppTwo"), "delete app should fail with deleted app")
|
||||||
|
os.RemoveAll("TestActionDeleteApp")
|
||||||
|
}
|
||||||
|
func TestActionDeleteRequest(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionDeleteRequest", ".sp9rk", "tests")
|
||||||
|
// simulate apps being created
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestApp",
|
||||||
|
Description: "this is a very cool app that does cool stuff",
|
||||||
|
Host: "http://localhost:3000",
|
||||||
|
})
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestAppTwo",
|
||||||
|
Description: "this is a very cool app that does cool stuff",
|
||||||
|
Host: "http://localhost:3001",
|
||||||
|
})
|
||||||
|
action.WriteRequestFiles(cfgPath, "TestApp", &action.RequestInfo{
|
||||||
|
Version: "1",
|
||||||
|
Name: "MyReq",
|
||||||
|
})
|
||||||
|
action.WriteRequestFiles(cfgPath, "TestAppTwo", &action.RequestInfo{
|
||||||
|
Version: "1",
|
||||||
|
Name: "AnotherReq",
|
||||||
|
})
|
||||||
|
// tests
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
assert.Error(t, RunWithArgs(app, "delete", "req"), "delete req should fail with no args")
|
||||||
|
assert.Error(t, RunWithArgs(app, "delete", "req", "--confirm", "MyReq"), "delete req should fail with no default app")
|
||||||
|
assert.NoError(t, RunWithArgs(app, "delete", "req", "--confirm", "-a", "TestApp", "MyReq"), "delete req should NOT fail with existing req")
|
||||||
|
assert.Error(t, RunWithArgs(app, "delete", "req", "-a", "TestApp", "MyReq"), "delete req should fail with deleted req")
|
||||||
|
assert.Error(t, RunWithArgs(app, "delete", "req", "-a", "TestApp", "../malicious/path"), "delete req should fail invalid name")
|
||||||
|
// create default app
|
||||||
|
os.WriteFile(path.Join(cfgPath, "current_app"), []byte("TestAppTwo"), 0700)
|
||||||
|
assert.Error(t, RunWithArgs(app, "delete", "req", "AnotherReq", "somethingElse"), "delete req should fail with >1 arg")
|
||||||
|
assert.Error(t, RunWithArgs(app, "delete", "req", "NotRealApp"), "delete req should fail with unknown req")
|
||||||
|
assert.NoError(t, RunWithArgs(app, "delete", "req", "--confirm", "AnotherReq"), "delete req should NOT fail with default app and existing req")
|
||||||
|
assert.Error(t, RunWithArgs(app, "delete", "req", "AnotherReq"), "delete req should fail with deleted req")
|
||||||
|
os.RemoveAll("TestActionDeleteRequest")
|
||||||
|
// tests
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionEditApp(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionEditApp", ".sp9rk", "tests")
|
||||||
|
// simulate apps being created
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestApp",
|
||||||
|
})
|
||||||
|
// tests
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
assert.NoError(t,
|
||||||
|
RunWithArgs(app, "edit", "app", "-d", "new description", "-u", "http://123.456.789", "TestApp"),
|
||||||
|
"edit app should not fail with 1 arg and flags",
|
||||||
|
)
|
||||||
|
contents, err := os.ReadFile(action.AppInfoFilePath(cfgPath, "TestApp"))
|
||||||
|
assert.NoError(t, err, "new .appinfo file should exist")
|
||||||
|
appinfo := new(action.AppInfo)
|
||||||
|
err = yaml.Unmarshal(contents, appinfo)
|
||||||
|
assert.NoError(t, err, "new .appinfo file should be correctly formed")
|
||||||
|
assert.EqualValues(t, "new description", appinfo.Description, "description should be updated")
|
||||||
|
assert.EqualValues(t, "http://123.456.789", appinfo.Host, "host should be updated")
|
||||||
|
assert.Error(t, RunWithArgs(app, "edit", "app"), "edit app should fail with no args")
|
||||||
|
assert.NoError(t, RunWithArgs(app, "edit", "app", "TestApp"), "edit app should NOT fail with no flags")
|
||||||
|
os.RemoveAll("TestActionEditApp")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionEditRequest(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionEditRequest", ".sp9rk", "tests")
|
||||||
|
// simulate apps being created
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestApp",
|
||||||
|
})
|
||||||
|
action.WriteRequestFiles(cfgPath, "TestApp", &action.RequestInfo{
|
||||||
|
Name: "Req",
|
||||||
|
})
|
||||||
|
action.WriteRequestFiles(cfgPath, "TestApp", &action.RequestInfo{
|
||||||
|
Name: "ReqTwo",
|
||||||
|
Path: "/path",
|
||||||
|
Description: "hello",
|
||||||
|
Body: "request body",
|
||||||
|
Method: "POST",
|
||||||
|
Headers: []string{"Header: One"},
|
||||||
|
})
|
||||||
|
os.WriteFile(path.Join(cfgPath, "current_app"), []byte("TestApp"), 0700)
|
||||||
|
// tests
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
assert.NoError(t,
|
||||||
|
RunWithArgs(app, "edit", "req",
|
||||||
|
"-a", "TestApp",
|
||||||
|
"-d", "new description",
|
||||||
|
"-X", "POST",
|
||||||
|
"-b", "new body",
|
||||||
|
"-H", "X-Header: ABC",
|
||||||
|
"-p", "/new-path",
|
||||||
|
"Req",
|
||||||
|
),
|
||||||
|
"edit app should not fail with 1 arg and flags",
|
||||||
|
)
|
||||||
|
contents, err := os.ReadFile(path.Join(action.AppPath(cfgPath, "TestApp"), "Req.yml"))
|
||||||
|
assert.NoError(t, err, "new request file should exist")
|
||||||
|
reqinfo := new(action.RequestInfo)
|
||||||
|
err = yaml.Unmarshal(contents, reqinfo)
|
||||||
|
assert.NoError(t, err, "new request file should be correctly formed")
|
||||||
|
assert.EqualValues(t, "new description", reqinfo.Description, "description should be updated")
|
||||||
|
assert.EqualValues(t, "POST", reqinfo.Method, "method should be updated")
|
||||||
|
assert.EqualValues(t, "new body", reqinfo.Body, "body should be updated")
|
||||||
|
assert.EqualValues(t, "X-Header: ABC", reqinfo.Headers[0], "header(s) should be updated")
|
||||||
|
assert.EqualValues(t, "/new-path", reqinfo.Path, "path should be updated")
|
||||||
|
assert.Error(t, RunWithArgs(app, "edit", "req"), "edit req should fail with no args")
|
||||||
|
assert.NoError(t, RunWithArgs(app, "edit", "req", "ReqTwo"), "edit req should NOT fail with no flags")
|
||||||
|
assert.NoError(t, RunWithArgs(app, "edit", "req", "-d", "new description", "ReqTwo"), "edit req should NOT fail when updating description")
|
||||||
|
contents, _ = os.ReadFile(path.Join(action.AppPath(cfgPath, "TestApp"), "ReqTwo.yml"))
|
||||||
|
reqinfo = new(action.RequestInfo)
|
||||||
|
yaml.Unmarshal(contents, reqinfo)
|
||||||
|
assert.EqualValues(t, "Header: One", reqinfo.Headers[0], "header(s) should NOT be overwritten when not updated")
|
||||||
|
assert.EqualValues(t, "/path", reqinfo.Path, "path should NOT be overwritten when not updated")
|
||||||
|
assert.EqualValues(t, "request body", reqinfo.Body, "header(s) should NOT be overwritten when not updated")
|
||||||
|
assert.EqualValues(t, "POST", reqinfo.Method, "method should NOT be overwritten when not updated")
|
||||||
|
os.RemoveAll("TestActionEditRequest")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionListApps(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionListApps", ".sp9rk", "tests")
|
||||||
|
// simulate apps being created
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestApp",
|
||||||
|
})
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "AnotherApp",
|
||||||
|
Description: "another one",
|
||||||
|
})
|
||||||
|
// tests
|
||||||
|
expected := "AnotherApp: another one\nTestApp\n"
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
out, err := captureOutput(RunWithArgs, app, "list", "app")
|
||||||
|
assert.NoError(t, err, "list app should NOT generate an error")
|
||||||
|
assert.EqualValues(t, expected, out, "list app should be alphabetical")
|
||||||
|
os.RemoveAll("TestActionListApps")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionListRequests(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionListRequests", ".sp9rk", "tests")
|
||||||
|
// simulate apps being created
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestAppTwo",
|
||||||
|
})
|
||||||
|
action.WriteRequestFiles(cfgPath, "TestAppTwo", &action.RequestInfo{
|
||||||
|
Name: "Req",
|
||||||
|
})
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestApp",
|
||||||
|
})
|
||||||
|
action.WriteRequestFiles(cfgPath, "TestApp", &action.RequestInfo{
|
||||||
|
Name: "MyReq",
|
||||||
|
Description: "my request",
|
||||||
|
})
|
||||||
|
action.WriteRequestFiles(cfgPath, "TestApp", &action.RequestInfo{
|
||||||
|
Name: "AnotherReq",
|
||||||
|
})
|
||||||
|
os.WriteFile(path.Join(cfgPath, "current_app"), []byte("TestApp"), 0700)
|
||||||
|
// tests
|
||||||
|
expected := "AnotherReq\nMyReq: my request\n"
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
out, err := captureOutput(RunWithArgs, app, "list", "req")
|
||||||
|
assert.NoError(t, err, "list req should NOT generate an error")
|
||||||
|
assert.EqualValues(t, expected, out, "list req should succeed with default app")
|
||||||
|
expected = "Req\n"
|
||||||
|
out, err = captureOutput(RunWithArgs, app, "list", "req", "-a", "TestAppTwo")
|
||||||
|
assert.NoError(t, err, "list req should NOT generate an error")
|
||||||
|
assert.EqualValues(t, expected, out, "list req should succeed with specific app")
|
||||||
|
os.RemoveAll("TestActionListRequests")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionListAll(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionListAll", ".sp9rk", "tests")
|
||||||
|
// simulate apps being created
|
||||||
|
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
|
||||||
|
expected := "No applications or requests\n"
|
||||||
|
out, err := captureOutput(RunWithArgs, app, "list")
|
||||||
|
assert.NoError(t, err, "list all with nothing loaded should NOT generate an error")
|
||||||
|
assert.EqualValues(t, expected, out, "list should generate expected output")
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestAppTwo",
|
||||||
|
})
|
||||||
|
expected = "TestAppTwo\n"
|
||||||
|
out, err = captureOutput(RunWithArgs, app, "list")
|
||||||
|
assert.NoError(t, err, "list req should NOT generate an error")
|
||||||
|
assert.EqualValues(t, expected, out, "list should generate expected output")
|
||||||
|
|
||||||
|
action.WriteRequestFiles(cfgPath, "TestAppTwo", &action.RequestInfo{
|
||||||
|
Name: "Req",
|
||||||
|
})
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestApp",
|
||||||
|
})
|
||||||
|
action.WriteRequestFiles(cfgPath, "TestApp", &action.RequestInfo{
|
||||||
|
Name: "MyReq",
|
||||||
|
Description: "my request",
|
||||||
|
})
|
||||||
|
action.WriteRequestFiles(cfgPath, "TestApp", &action.RequestInfo{
|
||||||
|
Name: "AnotherReq",
|
||||||
|
})
|
||||||
|
expected = `TestApp:
|
||||||
|
- AnotherReq
|
||||||
|
- MyReq: my request
|
||||||
|
TestAppTwo:
|
||||||
|
- Req
|
||||||
|
`
|
||||||
|
out, err = captureOutput(RunWithArgs, app, "list")
|
||||||
|
assert.NoError(t, err, "list req should NOT generate an error")
|
||||||
|
assert.EqualValues(t, expected, out, "list should generate expected output")
|
||||||
|
os.RemoveAll("TestActionListAll")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionAppInfo(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionAppInfo", ".sp9rk", "tests")
|
||||||
|
// simulate apps being created
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestApp",
|
||||||
|
Description: "test application",
|
||||||
|
Host: "http://123.456.789",
|
||||||
|
})
|
||||||
|
// tests
|
||||||
|
expected := `TestApp:
|
||||||
|
Description: test application
|
||||||
|
Host: http://123.456.789
|
||||||
|
`
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
out, err := captureOutput(RunWithArgs, app, "info", "app", "TestApp")
|
||||||
|
assert.NoError(t, err, "list req should NOT generate an error")
|
||||||
|
assert.EqualValues(t, expected, out, "list should generate expected output")
|
||||||
|
assert.Error(t, RunWithArgs(app, "info", "app"), "info app should generate an error with no args")
|
||||||
|
os.RemoveAll("TestActionAppInfo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionRequestInfo(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionRequestInfo", ".sp9rk", "tests")
|
||||||
|
// simulate apps being created
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestApp",
|
||||||
|
})
|
||||||
|
action.WriteRequestFiles(cfgPath, "TestApp", &action.RequestInfo{
|
||||||
|
Name: "MyReq",
|
||||||
|
Method: "GET",
|
||||||
|
Description: "my request",
|
||||||
|
Path: "/path",
|
||||||
|
Body: "request body",
|
||||||
|
Headers: []string{
|
||||||
|
"X-API-Key: ABC123",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// tests
|
||||||
|
expected := `MyReq:
|
||||||
|
description: my request
|
||||||
|
method: GET
|
||||||
|
path: /path
|
||||||
|
headers:
|
||||||
|
- 'X-API-Key: ABC123'
|
||||||
|
body: request body
|
||||||
|
`
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
out, err := captureOutput(RunWithArgs, app, "info", "req", "-a", "TestApp", "MyReq")
|
||||||
|
assert.NoError(t, err, "list req should NOT generate an error")
|
||||||
|
assert.EqualValues(t, expected, out, "list should generate expected output")
|
||||||
|
os.WriteFile(path.Join(cfgPath, "current_app"), []byte("TestApp"), 0700)
|
||||||
|
out, err = captureOutput(RunWithArgs, app, "info", "req", "MyReq")
|
||||||
|
assert.NoError(t, err, "list req should NOT generate an error")
|
||||||
|
assert.EqualValues(t, expected, out, "list should generate expected output with default app")
|
||||||
|
assert.Error(t, RunWithArgs(app, "info", "req"), "info req should generate an error with no args")
|
||||||
|
os.RemoveAll("TestActionRequestInfo")
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientMock struct{}
|
||||||
|
|
||||||
|
func (c *ClientMock) Do(req *http.Request) (*http.Response, error) {
|
||||||
|
return &http.Response{
|
||||||
|
Status: "OK",
|
||||||
|
StatusCode: 200,
|
||||||
|
Body: io.NopCloser(strings.NewReader("Hello, world!")),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
func TestActionCall(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionCall", ".sp9rk", "tests")
|
||||||
|
// simulate apps being created
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.EqualValues(t, "/path", r.URL.Path, "path is incorrect")
|
||||||
|
assert.EqualValues(t, r.Header.Get("X-API-Key"), "ABC123", "headers are incorrect")
|
||||||
|
assert.EqualValues(t, r.Method, "GET", "method is incorrect")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`Hello, World!`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestApp",
|
||||||
|
Host: server.URL,
|
||||||
|
})
|
||||||
|
action.WriteRequestFiles(cfgPath, "TestApp", &action.RequestInfo{
|
||||||
|
Name: "MyReq",
|
||||||
|
Method: "GET",
|
||||||
|
Description: "my request",
|
||||||
|
Path: "/path",
|
||||||
|
Body: "request body",
|
||||||
|
Headers: []string{
|
||||||
|
"X-API-Key: ABC123",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// test basic functionality
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
assert.Error(t, RunWithArgs(app, "call"), "call should fail with no argument")
|
||||||
|
assert.Error(t, RunWithArgs(app, "call", "MyReq"), "call should fail with no default app")
|
||||||
|
assert.Error(t, RunWithArgs(app, "call", "../bad/path"), "call should fail invalid request name")
|
||||||
|
assert.Error(t, RunWithArgs(app, "call", "-a", "../bad/app/path", "MyReq"), "call should fail with invalid app name")
|
||||||
|
assert.Error(t, RunWithArgs(app, "call", "-a", "TestApp", "FakeReq"), "call should fail with unknown app")
|
||||||
|
assert.Error(t, RunWithArgs(app, "call", "MyReq"), "call should fail with no default app")
|
||||||
|
output, err := captureOutput(RunWithArgs, app, "call", "-a", "TestApp", "MyReq")
|
||||||
|
assert.EqualValues(t, "Hello, World!\n", output, "call output should be response body")
|
||||||
|
assert.NoError(t, err, "call should succeed with explicit app")
|
||||||
|
os.WriteFile(path.Join(cfgPath, "current_app"), []byte("TestApp"), 0700)
|
||||||
|
output, err = captureOutput(RunWithArgs, app, "call", "MyReq")
|
||||||
|
assert.NoError(t, err, "call should succeed with default app")
|
||||||
|
assert.EqualValues(t, "Hello, World!\n", output, "call output should be response body")
|
||||||
|
|
||||||
|
// test extended functionality (flags)
|
||||||
|
output, err = captureOutput(RunWithArgs, app, "call", "--verbose", "MyReq")
|
||||||
|
assert.NoError(t, err, "call should succeed with verbose flag")
|
||||||
|
respData := new(action.VerboseCallResponse)
|
||||||
|
err = yaml.Unmarshal([]byte(output), respData)
|
||||||
|
assert.NoError(t, err, "output should be valid yaml")
|
||||||
|
assert.Equal(t, "GET "+server.URL+"/path", respData.Request, "request is incorrect")
|
||||||
|
assert.NotEmpty(t, respData.Latency, "latency must not be empty")
|
||||||
|
assert.Equal(t, "request body", respData.RequestBody, "request body is incorrect")
|
||||||
|
assert.Equal(t, "200 OK", respData.Status, "response status is incorrect")
|
||||||
|
assert.NotNil(t, respData.Headers, "request headers must not be nil")
|
||||||
|
assert.Equal(t, "ABC123", respData.Headers["X-API-Key"], "request headers are incorrect")
|
||||||
|
assert.Equal(t, "Hello, World!", respData.ResponseBody, "response body is incorrect")
|
||||||
|
os.RemoveAll("TestActionCall")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionCallFail(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionCallFail", ".sp9rk", "tests")
|
||||||
|
// make a new server that will make every request return >=400 status
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
w.Write([]byte(`Hello, World!`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestApp",
|
||||||
|
Host: server.URL,
|
||||||
|
})
|
||||||
|
action.WriteRequestFiles(cfgPath, "TestApp", &action.RequestInfo{
|
||||||
|
Name: "MyReq",
|
||||||
|
Method: "GET",
|
||||||
|
Description: "my request",
|
||||||
|
Path: "/path",
|
||||||
|
Body: "request body",
|
||||||
|
Headers: []string{
|
||||||
|
"X-API-Key: ABC123",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
os.WriteFile(path.Join(cfgPath, "current_app"), []byte("TestApp"), 0700)
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
output, err := captureOutput(RunWithArgs, app, "call", "--fail", "MyReq")
|
||||||
|
assert.Error(t, err, "call should fail with fail flag")
|
||||||
|
assert.Empty(t, output, "failed request should be silent with fail flag")
|
||||||
|
os.RemoveAll("TestActionCallFail")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO test redirects
|
||||||
|
func TestActionCallRedirects(t *testing.T) {
|
||||||
|
cfgPath := path.Join("TestActionCallLocation", ".sp9rk", "tests")
|
||||||
|
// make a new server that will make every request return >=400 status
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/hello" {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`Hello, World!`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(404)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/redirect-me" {
|
||||||
|
w.Header().Add("Location", server.URL+"/hello")
|
||||||
|
w.WriteHeader(http.StatusMovedPermanently)
|
||||||
|
w.Write([]byte(`Don't follow me!`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(404)
|
||||||
|
}))
|
||||||
|
defer server2.Close()
|
||||||
|
|
||||||
|
action.WriteAppFiles(cfgPath, &action.AppInfo{
|
||||||
|
Name: "TestApp",
|
||||||
|
Host: server2.URL,
|
||||||
|
})
|
||||||
|
action.WriteRequestFiles(cfgPath, "TestApp", &action.RequestInfo{
|
||||||
|
Name: "MyReq",
|
||||||
|
Method: "GET",
|
||||||
|
Description: "my request",
|
||||||
|
Path: "/redirect-me",
|
||||||
|
})
|
||||||
|
os.WriteFile(path.Join(cfgPath, "current_app"), []byte("TestApp"), 0700)
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
output, err := captureOutput(RunWithArgs, app, "call", "MyReq")
|
||||||
|
assert.NoError(t, err, "call should not fail when following valid redirect")
|
||||||
|
assert.Equal(t, "Hello, World!\n", output, "output should be equal to response body")
|
||||||
|
output, err = captureOutput(RunWithArgs, app, "call", "--no-redirect", "MyReq")
|
||||||
|
assert.NoError(t, err, "call should not fail when not following redirect")
|
||||||
|
assert.Equal(t, "Don't follow me!\n", output, "output should be equal to response body")
|
||||||
|
os.RemoveAll("TestActionCallLocation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper functions for testing
|
||||||
|
func RunWithArgs(app *cli.App, args ...string) error {
|
||||||
|
a := os.Args[0:1]
|
||||||
|
a = append(a, args...)
|
||||||
|
return app.Run(a)
|
||||||
|
}
|
||||||
|
func captureOutput(f func(*cli.App, ...string) error, app *cli.App, args ...string) (string, error) {
|
||||||
|
orig := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
err := f(app, args...)
|
||||||
|
os.Stdout = orig
|
||||||
|
w.Close()
|
||||||
|
out, _ := io.ReadAll(r)
|
||||||
|
return string(out), err
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestInfo struct {
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Method string `yaml:"method"`
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Headers []string `yaml:"headers"`
|
||||||
|
Body string `yaml:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteRequestFiles(cfgPath, app string, req *RequestInfo) error {
|
||||||
|
data, err := yaml.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to marshal data")
|
||||||
|
}
|
||||||
|
path := path.Join(AppPath(cfgPath, app), req.Name+".yml")
|
||||||
|
return os.WriteFile(path, data, 0700)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppInfo struct {
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteAppFiles(cfgPath string, app *AppInfo) error {
|
||||||
|
data, err := yaml.Marshal(app)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to marshal data")
|
||||||
|
}
|
||||||
|
err = os.MkdirAll(AppPath(cfgPath, app.Name), 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.WriteFile(AppInfoFilePath(cfgPath, app.Name), data, 0700)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(AppPath(cfgPath, app.Name))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// If the --app flag is set, returns that app. Otherwise, returns the default app.
|
||||||
|
// If no default exists, returns an error.
|
||||||
|
func getApp(cfgPath string, ctx *cli.Context) (string, error) {
|
||||||
|
var app string
|
||||||
|
if ctx.String("app") == "" {
|
||||||
|
app = currentApp(cfgPath)
|
||||||
|
if app == "" {
|
||||||
|
return "", errors.New("application does not exist")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app = ctx.String("app")
|
||||||
|
}
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// "" if no current app is set
|
||||||
|
func currentApp(cfgPath string) string {
|
||||||
|
if _, err := os.Stat(path.Join(cfgPath, "current_app")); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
app, err := os.ReadFile(path.Join(cfgPath, "current_app"))
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TRUE if string is alphanumeric with - or _
|
||||||
|
func valid(name string) bool {
|
||||||
|
return regexp.MustCompile(`^[a-zA-Z0-9_-]*$`).MatchString(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TRUE if app folder exists
|
||||||
|
func appExists(cfgPath, app string) bool {
|
||||||
|
_, err := os.Stat(AppPath(cfgPath, app))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfirmPrompt() bool {
|
||||||
|
fmt.Print("Are you sure? [y/N]: ")
|
||||||
|
r := bufio.NewReader(os.Stdin)
|
||||||
|
res, err := r.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
// if empty, default to N
|
||||||
|
if len(res) < 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.ToLower(strings.TrimSpace(res))[0] == 'y'
|
||||||
|
}
|
||||||
|
|
||||||
|
func AppPath(cfgPath, app string) string {
|
||||||
|
return path.Join(cfgPath, "apps", app)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AppInfoFilePath(cfgPath, app string) string {
|
||||||
|
return path.Join(AppPath(cfgPath, app), ".appinfo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReqPath(cfgPath, app, req string) string {
|
||||||
|
return path.Join(cfgPath, "apps", app, req+".yml")
|
||||||
|
}
|
||||||
@ -0,0 +1,318 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/gabehf/sp9rk/action"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(cfgPath string, httpClient http.Client) *cli.App {
|
||||||
|
|
||||||
|
// prepare necessary directories
|
||||||
|
if _, err := os.Stat(cfgPath); err != nil {
|
||||||
|
err := os.MkdirAll(cfgPath, 0700)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprint(os.Stderr, "failed to create configuration directory")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(action.AppPath(cfgPath, "")); err != nil {
|
||||||
|
err := os.MkdirAll(action.AppPath(cfgPath, ""), 0700)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprint(os.Stderr, "failed to create apps directory")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// prepare config files
|
||||||
|
if _, err := os.Stat(path.Join(cfgPath, "current_app")); err != nil {
|
||||||
|
err := os.WriteFile(path.Join(cfgPath, "current_app"), []byte(""), 0700)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprint(os.Stderr, "failed to create required configuration file")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugFlag := "debug"
|
||||||
|
confirmFlag := "confirm"
|
||||||
|
appFlag := []string{"app", "a"}
|
||||||
|
descriptionFlag := []string{"description", "d"}
|
||||||
|
hostFlag := []string{"host", "u"}
|
||||||
|
methodFlag := []string{"method", "X"}
|
||||||
|
pathFlag := []string{"path", "p"}
|
||||||
|
bodyFlag := []string{"body", "b"}
|
||||||
|
headerFlag := []string{"header", "H"}
|
||||||
|
verboseFlag := []string{"verbose", "v"}
|
||||||
|
noRedirectFlag := []string{"no-redirect", "n"}
|
||||||
|
failFlag := []string{"fail", "f"}
|
||||||
|
|
||||||
|
return &cli.App{
|
||||||
|
Name: "sp9rk",
|
||||||
|
Usage: "Automate your API calls in the command line",
|
||||||
|
Version: "v0.0.1",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: debugFlag,
|
||||||
|
Usage: "enable debug output",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(*cli.Context) error {
|
||||||
|
fmt.Print(` ___ _
|
||||||
|
___ _ __ / _ \ _ __| | __
|
||||||
|
/ __| '_ \ (_) | '__| |/ /
|
||||||
|
\__ \ |_) \__, | | | <
|
||||||
|
|___/ .__/ /_/|_| |_|\_\
|
||||||
|
|_|
|
||||||
|
|
||||||
|
Automate your API calls in the command line!
|
||||||
|
Type 'sp9rk help' for more information.`)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "create",
|
||||||
|
Usage: "create applications or requests",
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "app",
|
||||||
|
Usage: "create an application",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: descriptionFlag[0],
|
||||||
|
Aliases: descriptionFlag[1:],
|
||||||
|
Usage: "",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: hostFlag[0],
|
||||||
|
Aliases: hostFlag[1:],
|
||||||
|
Usage: "specify the application's host address",
|
||||||
|
Value: "http://localhost",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: action.CreateApplication(cfgPath),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "request",
|
||||||
|
Aliases: []string{"req"},
|
||||||
|
Usage: "create a request within an application",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: appFlag[0],
|
||||||
|
Aliases: appFlag[1:],
|
||||||
|
Usage: "",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: methodFlag[0],
|
||||||
|
Aliases: methodFlag[1:],
|
||||||
|
Value: "GET",
|
||||||
|
Usage: "",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: pathFlag[0],
|
||||||
|
Aliases: pathFlag[1:],
|
||||||
|
Value: "/",
|
||||||
|
Usage: "",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: descriptionFlag[0],
|
||||||
|
Aliases: descriptionFlag[1:],
|
||||||
|
Usage: "",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: bodyFlag[0],
|
||||||
|
Aliases: bodyFlag[1:],
|
||||||
|
Usage: "",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: headerFlag[0],
|
||||||
|
Aliases: headerFlag[1:],
|
||||||
|
Usage: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: action.CreateRequest(cfgPath),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "edit",
|
||||||
|
Usage: "edit an existing application or request",
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "app",
|
||||||
|
Usage: "edit an application",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: descriptionFlag[0],
|
||||||
|
Aliases: descriptionFlag[1:],
|
||||||
|
Usage: "",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: hostFlag[0],
|
||||||
|
Aliases: hostFlag[1:],
|
||||||
|
Usage: "specify the application's host address",
|
||||||
|
Value: "http://localhost",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: action.EditApplication(cfgPath),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "request",
|
||||||
|
Aliases: []string{"req"},
|
||||||
|
Usage: "edit a request within an application",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: appFlag[0],
|
||||||
|
Aliases: appFlag[1:],
|
||||||
|
Usage: "",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: methodFlag[0],
|
||||||
|
Aliases: methodFlag[1:],
|
||||||
|
Usage: "",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: pathFlag[0],
|
||||||
|
Aliases: pathFlag[1:],
|
||||||
|
Usage: "",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: descriptionFlag[0],
|
||||||
|
Aliases: descriptionFlag[1:],
|
||||||
|
Usage: "",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: bodyFlag[0],
|
||||||
|
Aliases: bodyFlag[1:],
|
||||||
|
Usage: "",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: headerFlag[0],
|
||||||
|
Aliases: headerFlag[1:],
|
||||||
|
Usage: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: action.EditRequest(cfgPath),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "delete",
|
||||||
|
Usage: "delete applications or requests",
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "app",
|
||||||
|
Usage: "delete an application and all associated requests",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: confirmFlag,
|
||||||
|
Usage: "skips the confirmation request and immediately deletes the resource",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: action.DeleteApplication(cfgPath),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "request",
|
||||||
|
Aliases: []string{"req"},
|
||||||
|
Usage: "delete a request within an application",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: confirmFlag,
|
||||||
|
Usage: "skips the confirmation request and immediately deletes the resource",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: appFlag[0],
|
||||||
|
Aliases: appFlag[1:],
|
||||||
|
Usage: "specify an application",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: action.DeleteRequest(cfgPath),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "list",
|
||||||
|
Usage: "list out applications or requests",
|
||||||
|
Action: action.ListAll(cfgPath),
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "app",
|
||||||
|
Usage: "list saved applications",
|
||||||
|
Action: action.ListApplications(cfgPath),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "request",
|
||||||
|
Aliases: []string{"req"},
|
||||||
|
Usage: "list requests within an application",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: appFlag[0],
|
||||||
|
Aliases: appFlag[1:],
|
||||||
|
Usage: "specify an application",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: action.ListRequests(cfgPath),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "info",
|
||||||
|
Usage: "info of an application or request",
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "app",
|
||||||
|
Usage: "list saved applications",
|
||||||
|
Action: action.InfoApplication(cfgPath),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "request",
|
||||||
|
Aliases: []string{"req"},
|
||||||
|
Usage: "list requests within an application",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: appFlag[0],
|
||||||
|
Aliases: appFlag[1:],
|
||||||
|
Usage: "specify an application",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: action.InfoRequest(cfgPath),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "switch",
|
||||||
|
Usage: "set your current app",
|
||||||
|
Action: action.Switch(cfgPath),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "call",
|
||||||
|
Usage: "make a request",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: appFlag[0],
|
||||||
|
Aliases: appFlag[1:],
|
||||||
|
Usage: "specify an application",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: verboseFlag[0],
|
||||||
|
Aliases: verboseFlag[1:],
|
||||||
|
Usage: "enable verbose output",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: failFlag[0],
|
||||||
|
Aliases: failFlag[1:],
|
||||||
|
Usage: "fail silently",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: noRedirectFlag[0],
|
||||||
|
Aliases: noRedirectFlag[1:],
|
||||||
|
Usage: "follow redirects",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: action.Call(cfgPath, httpClient),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
module github.com/gabehf/sp9rk
|
||||||
|
|
||||||
|
go 1.22.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
|
github.com/urfave/cli/v2 v2.27.2
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
|
||||||
|
)
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
|
||||||
|
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/gabehf/sp9rk/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var cfgPath string
|
||||||
|
if os.Getenv("SP9RK_CONFIG_PATH") != "" {
|
||||||
|
cfgPath = os.Getenv("SP9RK_CONFIG_PATH")
|
||||||
|
} else {
|
||||||
|
homedir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprint(os.Stderr, "failed to retrieve user config directory")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cfgPath = path.Join(homedir, "sp9rk")
|
||||||
|
}
|
||||||
|
// init app
|
||||||
|
app := app.New(cfgPath, http.Client{})
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
fmt.Fprint(os.Stderr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue