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 }