Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions internal/account/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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")
Expand Down
20 changes: 20 additions & 0 deletions internal/account/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package account
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -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())
}
}
33 changes: 0 additions & 33 deletions internal/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -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
Expand Down
18 changes: 0 additions & 18 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 0 additions & 6 deletions internal/dispatch/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 1 addition & 8 deletions internal/dispatch/dispatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package dispatch
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"slices"
"strings"
"testing"
Expand Down Expand Up @@ -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: "<id>", Summary: "List a thing", Run: func(session api.Session, arguments []string) error {
Expand Down Expand Up @@ -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 {
Expand Down
130 changes: 130 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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)
}
}
}
}