diff --git a/api/db_test.go b/api/db_test.go
new file mode 100644
index 0000000..b5083d0
--- /dev/null
+++ b/api/db_test.go
@@ -0,0 +1,59 @@
+/*
+Copyright © 2023 Thomas von Dein
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package api
+
+import (
+ "github.com/tlinden/cenophane/cfg"
+ "os"
+ "testing"
+)
+
+func finalize(db *Db) {
+ if db.bolt != nil {
+ db.Close()
+ }
+ if _, err := os.Stat(db.cfg.DbFile); err == nil {
+ os.Remove(db.cfg.DbFile)
+ }
+}
+
+func TestNew(t *testing.T) {
+ var tests = []struct {
+ name string
+ file string
+ wantfail bool
+ }{
+ {"opennew", "test.db", false},
+ {"openfail", "/hopefully/not/existing/directory/test.db", true},
+ }
+
+ for _, tt := range tests {
+ c := &cfg.Config{DbFile: tt.file}
+ t.Run(tt.name, func(t *testing.T) {
+ db, err := NewDb(c)
+ defer finalize(db)
+ if err != nil && !tt.wantfail {
+ t.Errorf("expected: &Db{}, got err: " + err.Error())
+ }
+
+ if err == nil && tt.wantfail {
+ t.Errorf("expected: fail, got &Db{}")
+ }
+ })
+ }
+}
diff --git a/upctl/cmd/delete.go b/upctl/cmd/delete.go
deleted file mode 100644
index 74d079d..0000000
--- a/upctl/cmd/delete.go
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
-Copyright © 2023 Thomas von Dein
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-package cmd
-
-import (
- "errors"
- "github.com/spf13/cobra"
- "github.com/tlinden/cenophane/upctl/cfg"
- "github.com/tlinden/cenophane/upctl/lib"
-)
-
-func DeleteCommand(conf *cfg.Config) *cobra.Command {
- var deleteCmd = &cobra.Command{
- Use: "delete [options] ",
- Short: "Delete an upload",
- Long: `Delete an upload identified by its id`,
- RunE: func(cmd *cobra.Command, args []string) error {
- if len(args) == 0 {
- return errors.New("No id specified to delete!")
- }
-
- // errors at this stage do not cause the usage to be shown
- cmd.SilenceUsage = true
-
- return lib.Delete(conf, args)
- },
- }
-
- deleteCmd.Aliases = append(deleteCmd.Aliases, "rm")
- deleteCmd.Aliases = append(deleteCmd.Aliases, "d")
-
- return deleteCmd
-}
diff --git a/upctl/cmd/describe.go b/upctl/cmd/describe.go
deleted file mode 100644
index f38a1a7..0000000
--- a/upctl/cmd/describe.go
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
-Copyright © 2023 Thomas von Dein
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-package cmd
-
-import (
- "errors"
- "github.com/spf13/cobra"
- "github.com/tlinden/cenophane/upctl/cfg"
- "github.com/tlinden/cenophane/upctl/lib"
-)
-
-func DescribeCommand(conf *cfg.Config) *cobra.Command {
- var listCmd = &cobra.Command{
- Use: "describe [options] upload-id",
- Long: "Show detailed informations about an upload object.",
- Short: `Describe an upload.`,
- RunE: func(cmd *cobra.Command, args []string) error {
- if len(args) == 0 {
- return errors.New("No id specified to delete!")
- }
-
- // errors at this stage do not cause the usage to be shown
- cmd.SilenceUsage = true
-
- return lib.Describe(conf, args)
- },
- }
-
- listCmd.Aliases = append(listCmd.Aliases, "des")
- listCmd.Aliases = append(listCmd.Aliases, "info")
- listCmd.Aliases = append(listCmd.Aliases, "i")
-
- return listCmd
-}
diff --git a/upctl/cmd/download.go b/upctl/cmd/download.go
deleted file mode 100644
index ed12624..0000000
--- a/upctl/cmd/download.go
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
-Copyright © 2023 Thomas von Dein
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-package cmd
-
-import (
- "errors"
- "github.com/spf13/cobra"
- "github.com/tlinden/cenophane/upctl/cfg"
- "github.com/tlinden/cenophane/upctl/lib"
-)
-
-func DownloadCommand(conf *cfg.Config) *cobra.Command {
- var listCmd = &cobra.Command{
- Use: "download [options] upload-id",
- Long: "Download the file associated with an upload object.",
- Short: `Download a file.`,
- RunE: func(cmd *cobra.Command, args []string) error {
- if len(args) == 0 {
- return errors.New("No id specified to delete!")
- }
-
- // errors at this stage do not cause the usage to be shown
- cmd.SilenceUsage = true
-
- return lib.Download(conf, args)
- },
- }
-
- listCmd.Aliases = append(listCmd.Aliases, "down")
- listCmd.Aliases = append(listCmd.Aliases, "get")
- listCmd.Aliases = append(listCmd.Aliases, "g")
- listCmd.Aliases = append(listCmd.Aliases, "fetch")
-
- return listCmd
-}
diff --git a/upctl/cmd/list.go b/upctl/cmd/list.go
deleted file mode 100644
index 4d9ac65..0000000
--- a/upctl/cmd/list.go
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
-Copyright © 2023 Thomas von Dein
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-package cmd
-
-import (
- "github.com/spf13/cobra"
- "github.com/tlinden/cenophane/upctl/cfg"
- "github.com/tlinden/cenophane/upctl/lib"
-)
-
-func ListCommand(conf *cfg.Config) *cobra.Command {
- var listCmd = &cobra.Command{
- Use: "list [options] [file ..]",
- Short: "List uploads",
- Long: `List uploads.`,
- RunE: func(cmd *cobra.Command, args []string) error {
- // errors at this stage do not cause the usage to be shown
- cmd.SilenceUsage = true
-
- return lib.List(conf, args)
- },
- }
-
- // options
- listCmd.PersistentFlags().StringVarP(&conf.Apicontext, "apicontext", "", "", "Filter by given API context")
-
- listCmd.Aliases = append(listCmd.Aliases, "ls")
- listCmd.Aliases = append(listCmd.Aliases, "l")
-
- return listCmd
-}
diff --git a/upctl/cmd/subcommands.go b/upctl/cmd/subcommands.go
new file mode 100644
index 0000000..45093a6
--- /dev/null
+++ b/upctl/cmd/subcommands.go
@@ -0,0 +1,145 @@
+/*
+Copyright © 2023 Thomas von Dein
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+package cmd
+
+import (
+ "errors"
+ "github.com/spf13/cobra"
+ "github.com/tlinden/cenophane/upctl/cfg"
+ "github.com/tlinden/cenophane/upctl/lib"
+ "os"
+)
+
+func UploadCommand(conf *cfg.Config) *cobra.Command {
+ var uploadCmd = &cobra.Command{
+ Use: "upload [options] [file ..]",
+ Short: "Upload files",
+ Long: `Upload files to an upload api.`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if len(args) == 0 {
+ return errors.New("No files specified to upload!")
+ }
+
+ // errors at this stage do not cause the usage to be shown
+ cmd.SilenceUsage = true
+
+ return lib.UploadFiles(os.Stdout, conf, args)
+ },
+ }
+
+ // options
+ uploadCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "", "Expire setting: asap or duration (accepted shortcuts: dmh)")
+
+ uploadCmd.Aliases = append(uploadCmd.Aliases, "up")
+ uploadCmd.Aliases = append(uploadCmd.Aliases, "u")
+
+ return uploadCmd
+}
+
+func ListCommand(conf *cfg.Config) *cobra.Command {
+ var listCmd = &cobra.Command{
+ Use: "list [options] [file ..]",
+ Short: "List uploads",
+ Long: `List uploads.`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ // errors at this stage do not cause the usage to be shown
+ cmd.SilenceUsage = true
+
+ return lib.List(os.Stdout, conf, args)
+ },
+ }
+
+ // options
+ listCmd.PersistentFlags().StringVarP(&conf.Apicontext, "apicontext", "", "", "Filter by given API context")
+
+ listCmd.Aliases = append(listCmd.Aliases, "ls")
+ listCmd.Aliases = append(listCmd.Aliases, "l")
+
+ return listCmd
+}
+
+func DeleteCommand(conf *cfg.Config) *cobra.Command {
+ var deleteCmd = &cobra.Command{
+ Use: "delete [options] ",
+ Short: "Delete an upload",
+ Long: `Delete an upload identified by its id`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if len(args) == 0 {
+ return errors.New("No id specified to delete!")
+ }
+
+ // errors at this stage do not cause the usage to be shown
+ cmd.SilenceUsage = true
+
+ return lib.Delete(os.Stdout, conf, args)
+ },
+ }
+
+ deleteCmd.Aliases = append(deleteCmd.Aliases, "rm")
+ deleteCmd.Aliases = append(deleteCmd.Aliases, "d")
+
+ return deleteCmd
+}
+
+func DescribeCommand(conf *cfg.Config) *cobra.Command {
+ var listCmd = &cobra.Command{
+ Use: "describe [options] upload-id",
+ Long: "Show detailed informations about an upload object.",
+ Short: `Describe an upload.`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if len(args) == 0 {
+ return errors.New("No id specified to delete!")
+ }
+
+ // errors at this stage do not cause the usage to be shown
+ cmd.SilenceUsage = true
+
+ return lib.Describe(os.Stdout, conf, args)
+ },
+ }
+
+ listCmd.Aliases = append(listCmd.Aliases, "des")
+ listCmd.Aliases = append(listCmd.Aliases, "info")
+ listCmd.Aliases = append(listCmd.Aliases, "i")
+
+ return listCmd
+}
+
+func DownloadCommand(conf *cfg.Config) *cobra.Command {
+ var listCmd = &cobra.Command{
+ Use: "download [options] upload-id",
+ Long: "Download the file associated with an upload object.",
+ Short: `Download a file.`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if len(args) == 0 {
+ return errors.New("No id specified to delete!")
+ }
+
+ // errors at this stage do not cause the usage to be shown
+ cmd.SilenceUsage = true
+
+ return lib.Download(os.Stdout, conf, args)
+ },
+ }
+
+ listCmd.Aliases = append(listCmd.Aliases, "down")
+ listCmd.Aliases = append(listCmd.Aliases, "get")
+ listCmd.Aliases = append(listCmd.Aliases, "g")
+ listCmd.Aliases = append(listCmd.Aliases, "fetch")
+
+ return listCmd
+}
diff --git a/upctl/cmd/upload.go b/upctl/cmd/upload.go
deleted file mode 100644
index a271692..0000000
--- a/upctl/cmd/upload.go
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-Copyright © 2023 Thomas von Dein
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-package cmd
-
-import (
- "errors"
- "github.com/spf13/cobra"
- "github.com/tlinden/cenophane/upctl/cfg"
- "github.com/tlinden/cenophane/upctl/lib"
-)
-
-func UploadCommand(conf *cfg.Config) *cobra.Command {
- var uploadCmd = &cobra.Command{
- Use: "upload [options] [file ..]",
- Short: "Upload files",
- Long: `Upload files to an upload api.`,
- RunE: func(cmd *cobra.Command, args []string) error {
- if len(args) == 0 {
- return errors.New("No files specified to upload!")
- }
-
- // errors at this stage do not cause the usage to be shown
- cmd.SilenceUsage = true
-
- return lib.UploadFiles(conf, args)
- },
- }
-
- // options
- uploadCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "", "Expire setting: asap or duration (accepted shortcuts: dmh)")
-
- uploadCmd.Aliases = append(uploadCmd.Aliases, "up")
- uploadCmd.Aliases = append(uploadCmd.Aliases, "u")
-
- return uploadCmd
-}
diff --git a/upctl/go.mod b/upctl/go.mod
index 6b4ff8d..6f997ff 100644
--- a/upctl/go.mod
+++ b/upctl/go.mod
@@ -14,6 +14,7 @@ require (
)
require (
+ github.com/alecthomas/repr v0.2.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/mock v1.6.0 // indirect
diff --git a/upctl/go.sum b/upctl/go.sum
index d75a6e1..cd00eca 100644
--- a/upctl/go.sum
+++ b/upctl/go.sum
@@ -38,6 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
+github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
diff --git a/upctl/lib/client.go b/upctl/lib/client.go
index b8befac..7e9e08f 100644
--- a/upctl/lib/client.go
+++ b/upctl/lib/client.go
@@ -27,6 +27,7 @@ import (
"github.com/schollz/progressbar/v3"
"github.com/tlinden/cenophane/common"
"github.com/tlinden/cenophane/upctl/cfg"
+ "io"
"mime"
"os"
"path/filepath"
@@ -51,6 +52,9 @@ type ListParams struct {
const Maxwidth = 10
+/*
+ Create a new request object for outgoing queries
+*/
func Setup(c *cfg.Config, path string) *Request {
client := req.C()
if c.Debug {
@@ -86,9 +90,12 @@ func Setup(c *cfg.Config, path string) *Request {
}
return &Request{Url: c.Endpoint + path, R: R}
-
}
+/*
+ Iterate over args, considering the elements are filenames, and add
+ them to the request.
+*/
func GatherFiles(rq *Request, args []string) error {
for _, file := range args {
info, err := os.Stat(file)
@@ -120,7 +127,42 @@ func GatherFiles(rq *Request, args []string) error {
return nil
}
-func UploadFiles(c *cfg.Config, args []string) error {
+/*
+ Check HTTP Response Code and validate JSON status output, if
+ any. Turns'em into a regular error
+*/
+func HandleResponse(c *cfg.Config, resp *req.Response) error {
+ // we expect a json response, extract the error, if any
+ r := Response{}
+
+ if err := json.Unmarshal([]byte(resp.String()), &r); err != nil {
+ // text output!
+ r.Message = resp.String()
+ }
+
+ if c.Debug {
+ trace := resp.Request.TraceInfo()
+ fmt.Println(trace.Blame())
+ fmt.Println("----------")
+ fmt.Println(trace)
+ }
+
+ if !r.Success {
+ if len(r.Message) == 0 {
+ if resp.Err != nil {
+ return resp.Err
+ } else {
+ return errors.New("Unknown error")
+ }
+ } else {
+ return errors.New(r.Message)
+ }
+ }
+
+ return nil
+}
+
+func UploadFiles(w io.Writer, c *cfg.Config, args []string) error {
// setup url, req.Request, timeout handling etc
rq := Setup(c, "/file/")
@@ -150,46 +192,14 @@ func UploadFiles(c *cfg.Config, args []string) error {
return err
}
- return RespondExtended(resp)
+ if err := HandleResponse(c, resp); err != nil {
+ return err
+ }
+
+ return RespondExtended(w, resp)
}
-func HandleResponse(c *cfg.Config, resp *req.Response) error {
- // we expect a json response, extract the error, if any
- r := Response{}
-
- if err := json.Unmarshal([]byte(resp.String()), &r); err != nil {
- // text output!
- r.Message = resp.String()
- }
-
- if c.Debug {
- trace := resp.Request.TraceInfo()
- fmt.Println(trace.Blame())
- fmt.Println("----------")
- fmt.Println(trace)
- }
-
- if !r.Success {
- if len(r.Message) == 0 {
- if resp.Err != nil {
- return resp.Err
- } else {
- return errors.New("Unknown error")
- }
- } else {
- return errors.New(r.Message)
- }
- }
-
- // all right
- if r.Message != "" {
- fmt.Println(r.Message)
- }
-
- return nil
-}
-
-func List(c *cfg.Config, args []string) error {
+func List(w io.Writer, c *cfg.Config, args []string) error {
rq := Setup(c, "/list/")
params := &ListParams{Apicontext: c.Apicontext}
@@ -201,10 +211,14 @@ func List(c *cfg.Config, args []string) error {
return err
}
- return RespondTable(resp)
+ if err := HandleResponse(c, resp); err != nil {
+ return err
+ }
+
+ return RespondTable(w, resp)
}
-func Delete(c *cfg.Config, args []string) error {
+func Delete(w io.Writer, c *cfg.Config, args []string) error {
for _, id := range args {
rq := Setup(c, "/file/"+id+"/")
@@ -218,13 +232,17 @@ func Delete(c *cfg.Config, args []string) error {
return err
}
- fmt.Printf("Upload %s successfully deleted.\n", id)
+ fmt.Fprintf(w, "Upload %s successfully deleted.\n", id)
}
return nil
}
-func Describe(c *cfg.Config, args []string) error {
+func Describe(w io.Writer, c *cfg.Config, args []string) error {
+ if len(args) == 0 {
+ return errors.New("No id provided!")
+ }
+
id := args[0] // we describe only 1 object
rq := Setup(c, "/upload/"+id+"/")
@@ -234,25 +252,37 @@ func Describe(c *cfg.Config, args []string) error {
return err
}
- return RespondExtended(resp)
-}
-
-func Download(c *cfg.Config, args []string) error {
- id := args[0]
-
- // progres bar
- bar := progressbar.Default(100)
-
- callback := func(info req.DownloadInfo) {
- if info.Response.Response != nil {
- bar.Add(1)
- }
+ if err := HandleResponse(c, resp); err != nil {
+ return err
}
+ return RespondExtended(w, resp)
+}
+
+func Download(w io.Writer, c *cfg.Config, args []string) error {
+ if len(args) == 0 {
+ return errors.New("No id provided!")
+ }
+
+ id := args[0]
+
rq := Setup(c, "/file/"+id+"/")
+
+ if !c.Silent {
+ // progres bar
+ bar := progressbar.Default(100)
+
+ callback := func(info req.DownloadInfo) {
+ if info.Response.Response != nil {
+ bar.Add(1)
+ }
+ }
+
+ rq.R.SetDownloadCallback(callback)
+ }
+
resp, err := rq.R.
SetOutputFile(id).
- SetDownloadCallback(callback).
Get(rq.Url)
if err != nil {
@@ -278,7 +308,7 @@ func Download(c *cfg.Config, args []string) error {
return fmt.Errorf("\nUnable to rename file: " + err.Error())
}
- fmt.Printf("%s successfully downloaded to file %s.", id, cleanfilename)
+ fmt.Fprintf(w, "%s successfully downloaded to file %s.", id, cleanfilename)
return nil
}
diff --git a/upctl/lib/client_test.go b/upctl/lib/client_test.go
index 578f38e..0a2e449 100644
--- a/upctl/lib/client_test.go
+++ b/upctl/lib/client_test.go
@@ -19,10 +19,17 @@ package lib
import (
//"github.com/alecthomas/repr"
+ "bytes"
"fmt"
"github.com/jarcoal/httpmock"
"github.com/tlinden/cenophane/upctl/cfg"
+ "io/ioutil"
"net/http"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
"testing"
)
@@ -33,23 +40,64 @@ type Unit struct {
apikey string // set to something else than "token" to fail auth
wantfail bool // true: expect to fail
files []string // path relative to ./t/
- sendcode int // for httpmock
- sendjson string // struct to respond with
- route string // dito
- method string // method to use
+ expect string // regex used to parse the output
+
+ sendcode int // for httpmock
+ sendjson string // struct to respond with
+ sendfile string // bare file content to be sent
+ route string // dito
+ method string // method to use
}
// simulate our cenophane server
func Intercept(tt Unit) {
httpmock.RegisterResponder(tt.method, endpoint+tt.route,
func(request *http.Request) (*http.Response, error) {
- respbody := fmt.Sprintf(tt.sendjson)
- resp := httpmock.NewStringResponse(tt.sendcode, respbody)
- resp.Header.Set("Content-Type", "application/json; charset=utf-8")
+ var resp *http.Response
+
+ if tt.sendfile != "" {
+ // simulate a file download
+ content, err := ioutil.ReadFile(tt.sendfile)
+ if err != nil {
+ panic(err) // should not happen
+ }
+
+ stat, err := os.Stat(tt.sendfile)
+ if err != nil {
+ panic(err) // should not happen as well
+ }
+
+ resp = httpmock.NewStringResponse(tt.sendcode, string(content))
+ resp.Header.Set("Content-Type", "text/markdown; charset=utf-8")
+ resp.Header.Set("Content-Length", strconv.Itoa(int(stat.Size())))
+ resp.Header.Set("Content-Disposition", "attachment; filename='t1'")
+ } else {
+ // simulate JSON response
+ resp = httpmock.NewStringResponse(tt.sendcode, tt.sendjson)
+ resp.Header.Set("Content-Type", "application/json; charset=utf-8")
+ }
+
return resp, nil
})
}
+// execute the actual test
+func Check(t *testing.T, tt Unit, w *bytes.Buffer, err error) {
+ testname := fmt.Sprintf("%s-%t", tt.name, tt.wantfail)
+
+ if err != nil && !tt.wantfail {
+ t.Errorf("%s failed! wantfail: %t, error: %s", testname, tt.wantfail, err.Error())
+ }
+
+ if tt.expect != "" {
+ got := strings.TrimSpace(w.String())
+ r := regexp.MustCompile(tt.expect)
+ if !r.MatchString(got) {
+ t.Errorf("%s failed! error: output does not match!\nexpect: %s\ngot:\n%s", testname, tt.expect, got)
+ }
+ }
+}
+
func TestUploadFiles(t *testing.T) {
conf := &cfg.Config{
Mock: true,
@@ -70,7 +118,17 @@ func TestUploadFiles(t *testing.T) {
method: "POST",
},
{
- name: "upload-nonexistent-file",
+ name: "upload-dir",
+ apikey: "token",
+ wantfail: false,
+ route: "/file/",
+ sendcode: 200,
+ sendjson: `{"success": true}`,
+ files: []string{"../t"}, // pwd is lib/ !
+ method: "POST",
+ },
+ {
+ name: "upload-catch-nonexistent-file",
apikey: "token",
wantfail: true,
route: "/file/",
@@ -80,7 +138,7 @@ func TestUploadFiles(t *testing.T) {
method: "POST",
},
{
- name: "upload-unauth",
+ name: "upload-catch-no-access",
apikey: "token",
wantfail: true,
route: "/file/",
@@ -89,16 +147,31 @@ func TestUploadFiles(t *testing.T) {
files: []string{"../t/t1"},
method: "POST",
},
+ {
+ name: "upload-check-output",
+ apikey: "token",
+ wantfail: false,
+ route: "/file/",
+ sendcode: 200,
+ sendjson: `{"uploads":[
+ {
+ "id":"cc2c965a","expire":"asap","file":"t1","members":["t1"],
+ "uploaded":1679396814.890502,"context":"foo","url":""
+ }
+ ],
+ "success":true,
+ "message":"Download url: http://localhost:8080/download/cc2c965a/t1",
+ "code":200}`,
+ files: []string{"../t/t1"}, // pwd is lib/ !
+ method: "POST",
+ expect: "Expire: On first access",
+ },
}
- for _, tt := range tests {
- testname := fmt.Sprintf("UploadFiles-%s-%t", tt.name, tt.wantfail)
- Intercept(tt)
- err := UploadFiles(conf, tt.files)
-
- if err != nil && !tt.wantfail {
- t.Errorf("%s failed! wantfail: %t, error: %s", testname, tt.wantfail, err.Error())
- }
+ for _, unit := range tests {
+ var w bytes.Buffer
+ Intercept(unit)
+ Check(t, unit, &w, UploadFiles(&w, conf, unit.files))
}
}
@@ -110,7 +183,18 @@ func TestList(t *testing.T) {
Silent: true,
}
- listing := `{"uploads":[{"id":"c8dh","expire":"asap","file":"t1","members":["t1"],"uploaded":1679318969.6434112,"context":"foo","url":""}],"success":true,"message":"","code":200}`
+ listing := `{"uploads":[
+ {
+ "id":"cc2c965a","expire":"asap","file":"t1","members":["t1"],
+ "uploaded":1679396814.890502,"context":"foo","url":""
+ }
+ ],
+ "success":true,
+ "message":"",
+ "code":200}`
+
+ listingnoaccess := `{"success":false,"message":"invalid context","code":503}`
+
tests := []Unit{
{
name: "list",
@@ -121,17 +205,205 @@ func TestList(t *testing.T) {
sendjson: listing,
files: []string{},
method: "GET",
+ expect: `cc2c965a\s*asap\s*foo\s*2023-03-21 12:06:54`, // expect tabular output
+ },
+ {
+ name: "list-catch-empty-json",
+ apikey: "token",
+ wantfail: true,
+ route: "/list/",
+ sendcode: 404,
+ sendjson: "",
+ files: []string{},
+ method: "GET",
+ },
+ {
+ name: "list-catch-no-access",
+ apikey: "token",
+ wantfail: true,
+ route: "/list/",
+ sendcode: 503,
+ sendjson: listingnoaccess,
+ files: []string{},
+ method: "GET",
},
}
- for _, tt := range tests {
- testname := fmt.Sprintf("List-%s-%t", tt.name, tt.wantfail)
- Intercept(tt)
- err := List(conf, []string{})
+ for _, unit := range tests {
+ var w bytes.Buffer
+ Intercept(unit)
+ Check(t, unit, &w, List(&w, conf, []string{}))
+ }
+}
- if err != nil && !tt.wantfail {
- t.Errorf("%s failed! wantfail: %t, error: %s", testname, tt.wantfail, err.Error())
- }
+func TestDescribe(t *testing.T) {
+ conf := &cfg.Config{
+ Mock: true,
+ Apikey: "token",
+ Endpoint: endpoint,
+ Silent: true,
}
+ listing := `{"uploads":[
+ {
+ "id":"cc2c965a","expire":"asap","file":"t1","members":["t1"],
+ "uploaded":1679396814.890502,"context":"foo","url":""
+ }
+ ],
+ "success":true,
+ "message":"",
+ "code":200}`
+
+ listingnoaccess := `{"success":false,"message":"invalid context","code":503}`
+
+ tests := []Unit{
+ {
+ name: "describe",
+ apikey: "token",
+ wantfail: false,
+ route: "/upload/",
+ sendcode: 200,
+ sendjson: listing,
+ files: []string{"cc2c965a"},
+ method: "GET",
+ expect: `Uploaded: 2023-03-21 12:06:54.890501888`,
+ },
+ {
+ name: "describe-catch-empty-json",
+ apikey: "token",
+ wantfail: true,
+ route: "/upload/",
+ sendcode: 200,
+ sendjson: "",
+ files: []string{"cc2c965a"},
+ method: "GET",
+ },
+ {
+ name: "describe-catch-no-access",
+ apikey: "token",
+ wantfail: true,
+ route: "/upload/",
+ sendcode: 503,
+ sendjson: listingnoaccess,
+ files: []string{"cc2c965a"},
+ method: "GET",
+ },
+ }
+
+ for _, unit := range tests {
+ var w bytes.Buffer
+ unit.route += unit.files[0] + "/"
+ Intercept(unit)
+ Check(t, unit, &w, Describe(&w, conf, unit.files))
+ }
+}
+
+func TestDelete(t *testing.T) {
+ conf := &cfg.Config{
+ Mock: true,
+ Apikey: "token",
+ Endpoint: endpoint,
+ Silent: true,
+ }
+
+ listingnoaccess := `{"success":false,"message":"invalid context","code":503}`
+
+ tests := []Unit{
+ {
+ name: "delete",
+ apikey: "token",
+ wantfail: false,
+ route: "/file/",
+ sendcode: 200,
+ sendjson: `{"success":true,"message":"","code":200}`,
+ files: []string{"cc2c965a"},
+ method: "DELETE",
+ expect: `Upload cc2c965a successfully deleted`,
+ },
+ {
+ name: "delete-catch-empty-json",
+ apikey: "token",
+ wantfail: true,
+ route: "/file/",
+ sendcode: 200,
+ sendjson: "",
+ files: []string{"cc2c965a"},
+ method: "DELETE",
+ },
+ {
+ name: "delete-catch-no-access",
+ apikey: "token",
+ wantfail: true,
+ route: "/file/",
+ sendcode: 503,
+ sendjson: listingnoaccess,
+ files: []string{"cc2c965a"},
+ method: "DELETE",
+ },
+ }
+
+ for _, unit := range tests {
+ var w bytes.Buffer
+ unit.route += unit.files[0] + "/"
+ Intercept(unit)
+ Check(t, unit, &w, Delete(&w, conf, unit.files))
+ }
+}
+
+func TestDownload(t *testing.T) {
+ conf := &cfg.Config{
+ Mock: true,
+ Apikey: "token",
+ Endpoint: endpoint,
+ Silent: true,
+ }
+
+ listingnoaccess := `{"success":false,"message":"invalid context","code":503}`
+
+ tests := []Unit{
+ {
+ name: "download",
+ apikey: "token",
+ wantfail: false,
+ route: "/file/",
+ sendcode: 200,
+ sendfile: "../t/t1",
+ files: []string{"cc2c965a"},
+ method: "GET",
+ expect: `cc2c965a successfully downloaded to file t1`,
+ },
+ {
+ name: "download-catch-empty-response",
+ apikey: "token",
+ wantfail: true,
+ route: "/file/",
+ sendcode: 200,
+ files: []string{"cc2c965a"},
+ method: "GET",
+ },
+ {
+ name: "download-catch-no-access",
+ apikey: "token",
+ wantfail: true,
+ route: "/file/",
+ sendcode: 503,
+ sendjson: listingnoaccess,
+ files: []string{"cc2c965a"},
+ method: "GET",
+ },
+ }
+
+ for _, unit := range tests {
+ var w bytes.Buffer
+ unit.route += unit.files[0] + "/"
+ Intercept(unit)
+ Check(t, unit, &w, Download(&w, conf, unit.files))
+
+ if unit.sendfile != "" {
+ file := filepath.Base(unit.sendfile)
+ if _, err := os.Stat(file); err == nil {
+ os.Remove(file)
+ }
+ }
+ }
}
diff --git a/upctl/lib/output.go b/upctl/lib/output.go
index 2d70b32..849a5d7 100644
--- a/upctl/lib/output.go
+++ b/upctl/lib/output.go
@@ -24,7 +24,8 @@ import (
"github.com/imroc/req/v3"
"github.com/olekukonko/tablewriter"
"github.com/tlinden/cenophane/common"
- "os"
+ "io"
+ "strings"
"time"
)
@@ -41,8 +42,9 @@ func prepareExpire(expire string, start common.Timestamp) string {
}
// generic table writer
-func WriteTable(headers []string, data [][]string) {
- table := tablewriter.NewWriter(os.Stdout)
+func WriteTable(w io.Writer, headers []string, data [][]string) {
+ tableString := &strings.Builder{}
+ table := tablewriter.NewWriter(tableString)
table.SetHeader(headers)
table.AppendBulk(data)
@@ -60,22 +62,24 @@ func WriteTable(headers []string, data [][]string) {
table.SetNoWhiteSpace(true)
table.Render()
+
+ fmt.Fprintln(w, tableString.String())
}
// output like psql \x
-func WriteExtended(uploads *common.Uploads) {
+func WriteExtended(w io.Writer, uploads *common.Uploads) {
format := fmt.Sprintf("%%%ds: %%s\n", Maxwidth)
// we shall only have 1 element, however, if we ever support more, here we go
for _, entry := range uploads.Entries {
expire := prepareExpire(entry.Expire, entry.Uploaded)
- fmt.Printf(format, "Id", entry.Id)
- fmt.Printf(format, "Expire", expire)
- fmt.Printf(format, "Context", entry.Context)
- fmt.Printf(format, "Uploaded", entry.Uploaded)
- fmt.Printf(format, "Filename", entry.File)
- fmt.Printf(format, "Url", entry.Url)
- fmt.Println()
+ fmt.Fprintf(w, format, "Id", entry.Id)
+ fmt.Fprintf(w, format, "Expire", expire)
+ fmt.Fprintf(w, format, "Context", entry.Context)
+ fmt.Fprintf(w, format, "Uploaded", entry.Uploaded)
+ fmt.Fprintf(w, format, "Filename", entry.File)
+ fmt.Fprintf(w, format, "Url", entry.Url)
+ fmt.Fprintln(w)
}
}
@@ -95,14 +99,14 @@ func GetUploadsFromResponse(resp *req.Response) (*common.Uploads, error) {
}
// turn the Uploads{} struct into a table and print it
-func RespondTable(resp *req.Response) error {
+func RespondTable(w io.Writer, resp *req.Response) error {
uploads, err := GetUploadsFromResponse(resp)
if err != nil {
return err
}
if uploads.Message != "" {
- fmt.Println(uploads.Message)
+ fmt.Fprintln(w, uploads.Message)
}
// tablewriter
@@ -113,23 +117,23 @@ func RespondTable(resp *req.Response) error {
})
}
- WriteTable([]string{"ID", "EXPIRE", "CONTEXT", "UPLOADED"}, data)
+ WriteTable(w, []string{"ID", "EXPIRE", "CONTEXT", "UPLOADED"}, data)
return nil
}
// turn the Uploads{} struct into xtnd output and print it
-func RespondExtended(resp *req.Response) error {
+func RespondExtended(w io.Writer, resp *req.Response) error {
uploads, err := GetUploadsFromResponse(resp)
if err != nil {
return err
}
if uploads.Message != "" {
- fmt.Println(uploads.Message)
+ fmt.Fprintln(w, uploads.Message)
}
- WriteExtended(uploads)
+ WriteExtended(w, uploads)
return nil
}