diff --git a/internal/account/account.go b/internal/account/account.go index fda4769..7b49f9e 100644 --- a/internal/account/account.go +++ b/internal/account/account.go @@ -156,6 +156,20 @@ func Delete(session api.Session, arguments []string) error { return errors.New("account delete takes no arguments") } + request, err := api.AuthenticatedRequest(session, http.MethodGet, "/", nil) + + if err != nil { + return err + } + + response, err := session.Client.Do(request) + + if err != nil { + return errors.New("the server could not be reached, check your connection") + } + + response.Body.Close() + fmt.Fprint(session.Out, "Delete your account, its logins, and your access to every fleet? This cannot be undone. [y/N] ") answer, _ := bufio.NewReader(session.In).ReadString('\n') @@ -167,13 +181,13 @@ func Delete(session api.Session, arguments []string) error { return nil } - request, err := api.AuthenticatedRequest(session, http.MethodDelete, "/account", nil) + request, err = api.AuthenticatedRequest(session, http.MethodDelete, "/account", nil) if err != nil { return err } - response, err := session.Client.Do(request) + response, err = session.Client.Do(request) if err != nil { return errors.New("the server could not be reached, check your connection") diff --git a/internal/account/account_test.go b/internal/account/account_test.go index eee7575..32cc467 100644 --- a/internal/account/account_test.go +++ b/internal/account/account_test.go @@ -3,6 +3,7 @@ package account import ( "fmt" "net/http" + "net/http/httptest" "os" "strings" "testing" @@ -374,3 +375,22 @@ func TestAccountDelete(t *testing.T) { }) } } + +func TestAccountDeleteAsksNothingWhenTheServerIsGone(t *testing.T) { + session, out := apitest.LoggedInSession(t, http.NewServeMux()) + + gone := httptest.NewServer(http.NotFoundHandler()) + gone.Close() + + session.Base = gone.URL + + err := Delete(session, nil) + + if err == nil || !strings.Contains(err.Error(), "could not be reached") { + t.Fatalf("error = %v, want it to mention the server could not be reached", err) + } + + if out.String() != "" { + t.Errorf("it asked %q before finding the server was gone", out.String()) + } +} diff --git a/internal/api/api_test.go b/internal/api/api_test.go index 4ca247b..b3bab3a 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "net/http" - "net/http/httptest" "os" "path/filepath" "runtime" @@ -108,38 +107,6 @@ func TestApiRequestBase(t *testing.T) { } } -func TestCheckServer(t *testing.T) { - reachable := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) - t.Cleanup(reachable.Close) - - unreachable := httptest.NewServer(http.NotFoundHandler()) - unreachable.Close() - tests := []struct { - name string - base string - wantError string - }{ - {name: "reachable", base: reachable.URL}, - {name: "unreachable", base: unreachable.URL, wantError: "the server could not be reached, check your connection"}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - session := api.NewSession(test.base, "test", strings.NewReader(""), &bytes.Buffer{}) - - err := api.CheckServer(session) - - if test.wantError != "" { - if err == nil || err.Error() != test.wantError { - t.Fatalf("error = %v, want %q", err, test.wantError) - } - } else if err != nil { - t.Fatal(err) - } - }) - } -} - func TestFetchFleetsFailures(t *testing.T) { tests := []struct { name string diff --git a/internal/api/client.go b/internal/api/client.go index 50f933b..aec8162 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -11,24 +11,6 @@ import ( "strings" ) -func CheckServer(session Session) error { - request, err := Request(session, http.MethodGet, "/", nil) - - if err != nil { - return err - } - - response, err := session.Client.Do(request) - - if err != nil { - return errors.New("the server could not be reached, check your connection") - } - - response.Body.Close() - - return nil -} - func Request(session Session, method string, path string, body io.Reader) (*http.Request, error) { request, err := http.NewRequest(method, strings.TrimSuffix(session.Base, "/")+path, body) diff --git a/internal/dispatch/dispatch.go b/internal/dispatch/dispatch.go index f0218d3..926c2f1 100644 --- a/internal/dispatch/dispatch.go +++ b/internal/dispatch/dispatch.go @@ -187,12 +187,6 @@ func Dispatch(sections []Section, version string, arguments []string, in io.Read return fmt.Errorf("%s is not available yet", entry.Name) } - err = api.CheckServer(session) - - if err != nil { - return err - } - err = entry.Run(session, rest) return err diff --git a/internal/dispatch/dispatch_test.go b/internal/dispatch/dispatch_test.go index 237f270..628661a 100644 --- a/internal/dispatch/dispatch_test.go +++ b/internal/dispatch/dispatch_test.go @@ -3,8 +3,6 @@ package dispatch import ( "bytes" "fmt" - "net/http" - "net/http/httptest" "slices" "strings" "testing" @@ -164,10 +162,6 @@ func TestResolve(t *testing.T) { } func TestDispatch(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) - - t.Cleanup(server.Close) - sections := []Section{ {Title: "Things", Commands: []Command{ {Name: "thing list", Arguments: "", Summary: "List a thing", Run: func(session api.Session, arguments []string) error { @@ -211,10 +205,9 @@ func TestDispatch(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - arguments := append([]string{"--server", server.URL}, test.arguments...) out := &bytes.Buffer{} - err := Dispatch(sections, "1.2.3", arguments, strings.NewReader(""), out) + err := Dispatch(sections, "1.2.3", test.arguments, strings.NewReader(""), out) if test.wantError != "" { if err == nil || err.Error() != test.wantError { diff --git a/main_test.go b/main_test.go index c4291e2..0baaf90 100644 --- a/main_test.go +++ b/main_test.go @@ -1,7 +1,16 @@ package main import ( + "bytes" + "go/parser" + "go/token" + "io/fs" + "path/filepath" + "slices" + "strings" "testing" + + "github.com/siliconwitchery/superstack-cli/internal/dispatch" ) func TestCommandTable(t *testing.T) { @@ -73,3 +82,124 @@ func TestOnlyPlannedCommandsAreUnimplemented(t *testing.T) { t.Errorf("%q is answered by dispatch but not in the table", name) } } + +func TestNoPartImportsAnother(t *testing.T) { + const module = "github.com/siliconwitchery/superstack-cli/internal/" + const fixtures = "api/apitest" + + // The graph docs/cli.md publishes: a part reaches api and nothing else, + // and only main reaches dispatch. + allowed := map[string][]string{ + "api": {}, + "api/apitest": {"api"}, + "dispatch": {"api"}, + "account": {"api"}, + "device": {"api"}, + "fleet": {"api"}, + "key": {"api"}, + "login": {"api"}, + "member": {"api"}, + } + + walk := func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + + if entry.IsDir() || !strings.HasSuffix(path, ".go") { + return nil + } + + owner := filepath.ToSlash(strings.TrimPrefix(filepath.Dir(path), "internal"+string(filepath.Separator))) + + permitted, known := allowed[owner] + + if !known { + t.Errorf("%s is a package the graph does not mention, add it to docs/cli.md and to this test", owner) + return nil + } + + file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly) + + if err != nil { + return err + } + + for _, imported := range file.Imports { + target := strings.Trim(imported.Path.Value, `"`) + + if !strings.HasPrefix(target, module) { + continue + } + + target = strings.TrimPrefix(target, module) + + if target == owner || slices.Contains(permitted, target) { + continue + } + + if target == fixtures && strings.HasSuffix(path, "_test.go") { + continue + } + + t.Errorf("%s imports %s, which the layout does not allow", path, target) + } + + return nil + } + + err := filepath.WalkDir("internal", walk) + + if err != nil { + t.Fatal(err) + } +} + +func TestTheTableWiresEveryCommandOffered(t *testing.T) { + wired := []string{ + "account balance", "account delete", "account topup", + "device claim", "device list", "device release", "device rename", + "fleet create", "fleet delete", "fleet list", "fleet rename", "fleet transfer", + "key create", "key list", "key revoke", + "login", "logout", + "member add", "member list", "member remove", + } + + offered := []string{} + + for _, section := range sections { + for _, entry := range section.Commands { + if entry.Run != nil { + offered = append(offered, entry.Name) + } + } + } + + slices.Sort(offered) + + if !slices.Equal(offered, wired) { + t.Errorf("the table wires %v, want %v", offered, wired) + } +} + +func TestHelpRendersTheRealTable(t *testing.T) { + out := &bytes.Buffer{} + + err := dispatch.Dispatch(sections, version, []string{"help"}, strings.NewReader(""), out) + + if err != nil { + t.Fatal(err) + } + + for _, section := range sections { + if !strings.Contains(out.String(), section.Title) { + t.Errorf("help leaves out the %q section", section.Title) + } + + for _, entry := range section.Commands { + if !strings.Contains(out.String(), entry.Name) { + t.Errorf("help leaves out %q", entry.Name) + } + } + } +}