Repository: nats-io/nsc
Pull request #695 (2025-01-04 22:59:52)

[TEST] increase coverage


Patch:
diff --git a/cmd/generatediagram.go b/cmd/generatediagram.go
index 0be20cc..d7ce746 100644
--- a/cmd/generatediagram.go
+++ b/cmd/generatediagram.go
@@ -27,17 +27,22 @@ import (
 	"github.com/spf13/cobra"
 )
 
+var accDetail bool
 var outputFile string
+var users, showKeys, detail bool
 
-func init() {
+func createDiagramCmd() *cobra.Command {
 	diagram := &cobra.Command{
 		Use:          "diagram",
 		Short:        "Generate diagrams for this store",
 		Args:         MaxArgs(0),
 		SilenceUsage: true,
 	}
-	accDetail := false
-	comp := &cobra.Command{
+	return diagram
+}
+
+func createComponentDiagreamCmd() *cobra.Command {
+	cmd := &cobra.Command{
 		Use:          "component",
 		Short:        "Generate a plantuml component diagram for this store",
 		Args:         MaxArgs(0),
@@ -47,10 +52,14 @@ func init() {
 			return componentDiagram(accDetail)
 		},
 	}
-	comp.Flags().BoolVarP(&accDetail, "detail", "", false, "Include account descriptions")
-	diagram.AddCommand(comp)
-	showKeys, detail, users := false, false, false
-	object := &cobra.Command{
+	cmd.Flags().BoolVarP(&accDetail, "detail", "", false, "Include account descriptions")
+	cmd.Flags().StringVarP(&outputFile, "output-file", "o", "--", "output file, '--' is stdout")
+
+	return cmd
+}
+
+func createObjectDiagramCmd() *cobra.Command {
+	cmd := &cobra.Command{
 		Use:          "object",
 		Short:        "Generate a plantuml object diagram for this store",
 		Args:         MaxArgs(0),
@@ -60,12 +69,20 @@ func init() {
 			return objectDiagram(users, showKeys, detail)
 		},
 	}
-	object.Flags().BoolVarP(&showKeys, "show-keys", "", false, "Include keys in diagram")
-	object.Flags().BoolVarP(&users, "users", "", false, "Include User")
-	object.Flags().BoolVarP(&detail, "detail", "", false, "Include empty/unlimited values")
-	diagram.AddCommand(object)
-	diagram.PersistentFlags().StringVarP(&outputFile, "output-file", "o", "--", "output file, '--' is stdout")
+
+	cmd.Flags().BoolVarP(&showKeys, "show-keys", "", false, "Include keys in diagram")
+	cmd.Flags().BoolVarP(&users, "users", "", false, "Include User")
+	cmd.Flags().BoolVarP(&detail, "detail", "", false, "Include empty/unlimited values")
+	cmd.Flags().StringVarP(&outputFile, "output-file", "o", "--", "output file, '--' is stdout")
+
+	return cmd
+}
+
+func init() {
+	diagram := createDiagramCmd()
 	generateCmd.AddCommand(diagram)
+	diagram.AddCommand(createComponentDiagreamCmd())
+	diagram.AddCommand(createObjectDiagramCmd())
 }
 
 const rename = "<&resize-width>"
diff --git a/cmd/usercontextparams.go b/cmd/usercontextparams.go
index 12ea261..52a3e8e 100644
--- a/cmd/usercontextparams.go
+++ b/cmd/usercontextparams.go
@@ -57,9 +57,13 @@ func (p *UserContextParams) SetDefaults(ctx ActionCtx) error {
 }
 
 func (p *UserContextParams) Edit(ctx ActionCtx) error {
-	config := GetConfig()
+	account := ctx.StoreCtx().Account.Name
+	if account == "" {
+		config := GetConfig()
+		account = config.Account
+	}
 	var err error
-	p.Name, err = PickUser(ctx.StoreCtx(), config.Account)
+	p.Name, err = PickUser(ctx.StoreCtx(), account)
 	if err != nil {
 		return err
 	}


Test patch:
diff --git a/cmd/accountusercontextparams_test.go b/cmd/accountusercontextparams_test.go
new file mode 100644
index 0000000..26afa4a
--- /dev/null
+++ b/cmd/accountusercontextparams_test.go
@@ -0,0 +1,66 @@
+package cmd
+
+import (
+	"github.com/nats-io/cliprompts/v2"
+	"github.com/nats-io/nsc/v2/cmd/store"
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+type tp struct {
+	AccountUserContextParams
+}
+
+func (p *tp) SetDefaults(ctx ActionCtx) error {
+	return p.AccountUserContextParams.SetDefaults(ctx)
+}
+
+func (p *tp) Validate(ctx ActionCtx) error {
+	return p.AccountUserContextParams.Validate(ctx)
+}
+
+func (p *tp) Load(ctx ActionCtx) error {
+	return nil
+}
+
+func (p *tp) Run(ctx ActionCtx) (store.Status, error) {
+	return nil, nil
+}
+
+func (p *tp) PreInteractive(ctx ActionCtx) error {
+	return p.AccountUserContextParams.Edit(ctx)
+}
+
+func (p *tp) PostInteractive(ctx ActionCtx) error {
+	return nil
+}
+
+func Test_AccountUserContextParams(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	ts.AddUser(t, "A", "a")
+	ts.AddAccount(t, "B")
+	ts.AddUser(t, "B", "b")
+
+	var params tp
+	cmd := &cobra.Command{
+		Use:          "ucp",
+		SilenceUsage: true,
+
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return RunAction(cmd, args, &params)
+		},
+	}
+
+	cliprompts.LogFn = t.Log
+	// A, a
+	inputs := []interface{}{0, 0}
+
+	_, _, err := ExecuteInteractiveCmd(cmd, inputs)
+	require.NoError(t, err)
+	require.Equal(t, "A", params.AccountContextParams.Name)
+	require.Equal(t, "a", params.UserContextParams.Name)
+}
diff --git a/cmd/deleteimport_test.go b/cmd/deleteimport_test.go
index 9a967b5..7b11b40 100644
--- a/cmd/deleteimport_test.go
+++ b/cmd/deleteimport_test.go
@@ -31,8 +31,8 @@ func Test_DeleteImport(t *testing.T) {
 	ts.AddExport(t, "A", jwt.Stream, "bar", 0, false)
 
 	ts.AddAccount(t, "B")
-	ts.AddImport(t, "A", "foo", "B")
-	ts.AddImport(t, "A", "bar", "B")
+	ts.AddImport(t, "A", jwt.Stream, "foo", "B")
+	ts.AddImport(t, "A", jwt.Stream, "bar", "B")
 
 	tests := CmdTests{
 		{createDeleteImportCmd(), []string{"delete", "import", "--account", "A"}, nil, []string{"account \"A\" doesn't have imports"}, true},
@@ -51,7 +51,7 @@ func Test_DeleteImportAccountRequired(t *testing.T) {
 	ts.AddAccount(t, "A")
 	ts.AddExport(t, "A", jwt.Stream, "foo", 0, false)
 	ts.AddAccount(t, "B")
-	ts.AddImport(t, "A", "foo", "B")
+	ts.AddImport(t, "A", jwt.Stream, "foo", "B")
 
 	GetConfig().SetAccount("")
 	_, _, err := ExecuteCmd(createDeleteImportCmd(), "--subject", "A")
@@ -68,8 +68,8 @@ func Test_DeleteImportInteractive(t *testing.T) {
 	ts.AddExport(t, "A", jwt.Stream, "bar", 0, false)
 
 	ts.AddAccount(t, "B")
-	ts.AddImport(t, "A", "foo", "B")
-	ts.AddImport(t, "A", "bar", "B")
+	ts.AddImport(t, "A", jwt.Stream, "foo", "B")
+	ts.AddImport(t, "A", jwt.Stream, "bar", "B")
 
 	input := []interface{}{1, 0, 0}
 	cmd := createDeleteImportCmd()
diff --git a/cmd/describeaccount_test.go b/cmd/describeaccount_test.go
index 21ec296..804ab46 100644
--- a/cmd/describeaccount_test.go
+++ b/cmd/describeaccount_test.go
@@ -18,11 +18,13 @@ package cmd
 import (
 	"encoding/json"
 	"fmt"
+	"github.com/nats-io/nkeys"
 	"os"
 	"path/filepath"
 	"runtime"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/nats-io/jwt/v2"
 	"github.com/stretchr/testify/require"
@@ -309,3 +311,87 @@ func TestDescribeAccount_Exports(t *testing.T) {
 	require.Contains(t, out, "| Account Token Position |")
 	require.Contains(t, out, "foo.bar.*.> | 3")
 }
+
+func TestDescribeAccountMore(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("running in windows - looking at output hangs")
+	}
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	ac.Description = "hello"
+	ac.InfoURL = "https://example.com"
+	_, signingKey, _ := CreateAccountKey(t)
+	ac.SigningKeys.Add(signingKey)
+
+	_, issuer, _ := CreateAccountKey(t)
+	scope := jwt.NewUserScope()
+	scope.Key = issuer
+	scope.Role = "nothing"
+	scope.Description = "no permissions"
+	scope.Template = jwt.UserPermissionLimits{
+		Permissions: jwt.Permissions{
+			Sub: jwt.Permission{Deny: []string{">"}},
+			Pub: jwt.Permission{Deny: []string{">"}},
+		},
+	}
+	ac.SigningKeys.AddScopedSigner(scope)
+
+	ac.Limits.JetStreamLimits = jwt.JetStreamLimits{DiskStorage: -1, MemoryStorage: -1}
+	ac.Limits.LeafNodeConn = 1
+
+	_, user, _ := CreateUserKey(t)
+	ac.Revocations = jwt.RevocationList{}
+	ac.Revocations.Revoke(user, time.Now())
+
+	ac.Trace = &jwt.MsgTrace{
+		Destination: "foo",
+		Sampling:    100,
+	}
+
+	ekp, err := nkeys.CreateUser()
+	require.NoError(t, err)
+	a, err := ekp.PublicKey()
+	require.NoError(t, err)
+	ac.Imports.Add(&jwt.Import{
+		Name:         "hello",
+		Subject:      "bar.>",
+		LocalSubject: "fromA.>",
+		Type:         jwt.Stream,
+		AllowTrace:   true,
+		Share:        true,
+		Account:      a,
+	})
+
+	ac.Mappings = make(map[jwt.Subject][]jwt.WeightedMapping)
+	ac.Mappings["mapfoo"] = []jwt.WeightedMapping{jwt.WeightedMapping{Subject: "map.>", Weight: 20, Cluster: "a"}}
+
+	token, err := ac.Encode(ts.OperatorKey)
+	require.NoError(t, err)
+	_, err = ts.Store.StoreClaim([]byte(token))
+	require.NoError(t, err)
+
+	out, _, err := ExecuteCmd(rootCmd, "describe", "account", "-n", "A")
+	require.NoError(t, err)
+
+	out = StripMultipleSpaces(out)
+	t.Log(out)
+	require.Contains(t, out, "| Description | hello")
+	require.Contains(t, out, "| Info Url | https://example.com")
+	// order of the key may be unexpected, just find the key
+	require.Contains(t, out, signingKey)
+	require.Contains(t, out, "| Max Disk Storage | Unlimited")
+	require.Contains(t, out, "| Max Mem Storage | Unlimited")
+	require.Contains(t, out, "| Max Leaf Node Connections | 1")
+	require.Contains(t, out, "| Revocations | 1")
+	require.Contains(t, out, "| Subject | foo")
+	require.Contains(t, out, "| Sampling | 100%")
+	require.Contains(t, out, "| hello | Stream | bar.> | fromA.>")
+	require.Contains(t, out, "| mapfoo | map.> | 20")
+
+	require.Contains(t, out, "| Key | "+issuer)
+	require.Contains(t, out, "| Role | nothing")
+	require.Contains(t, out, "| Description | no permissions")
+}
diff --git a/cmd/describeuser_test.go b/cmd/describeuser_test.go
index 2ad8317..f8b4dbf 100644
--- a/cmd/describeuser_test.go
+++ b/cmd/describeuser_test.go
@@ -24,7 +24,6 @@ import (
 	"testing"
 
 	"github.com/nats-io/jwt/v2"
-
 	"github.com/stretchr/testify/require"
 )
 
diff --git a/cmd/fixenv_test.go b/cmd/fixenv_test.go
index 16a7899..62d33c6 100644
--- a/cmd/fixenv_test.go
+++ b/cmd/fixenv_test.go
@@ -17,13 +17,12 @@ package cmd
 
 import (
 	"fmt"
-	"path/filepath"
-	"testing"
-	"time"
-
 	"github.com/nats-io/jwt/v2"
 	"github.com/nats-io/nsc/v2/cmd/store"
 	"github.com/stretchr/testify/require"
+	"path/filepath"
+	"testing"
+	"time"
 )
 
 func Test_FixRequiresInArg(t *testing.T) {
@@ -62,13 +61,6 @@ func Test_FixBasics(t *testing.T) {
 	require.NoError(t, err)
 	require.NoError(t, Write(filepath.Join(in, "o.jwt"), []byte(otok)))
 
-	// and another with a tag
-	oc.Tags.Add("test")
-	time.Sleep(time.Second)
-	otok2, err := oc.Encode(okp)
-	require.NoError(t, err)
-	require.NoError(t, Write(filepath.Join(in, "o2.jwt"), []byte(otok2)))
-
 	ask, apk, akp := CreateAccountKey(t)
 	require.NoError(t, Write(filepath.Join(in, "apk.nk"), ask))
 	ac := jwt.NewAccountClaims(apk)
@@ -85,8 +77,35 @@ func Test_FixBasics(t *testing.T) {
 	require.NoError(t, err)
 	require.NoError(t, Write(filepath.Join(in, "u.jwt"), []byte(utok)))
 
+	usk2, upk2, _ := CreateUserKey(t)
+	uc2 := jwt.NewUserClaims(upk2)
+	uc2.Name = "U2"
+	u2tok, err := uc2.Encode(akp)
+	require.NoError(t, err)
+	creds, err := jwt.FormatUserConfig(u2tok, usk2)
+	require.NoError(t, err)
+	require.NoError(t, Write(filepath.Join(in, "u2.creds"), creds))
+
+	// and copy of operator with a tag (and newer date)
+	time.Sleep(time.Second)
+
+	oc.Tags.Add("test")
+	otok2, err := oc.Encode(okp)
+	require.NoError(t, err)
+	require.NoError(t, Write(filepath.Join(in, "o2.jwt"), []byte(otok2)))
+
+	ac.Tags.Add("test")
+	atok, err = ac.Encode(okp)
+	require.NoError(t, err)
+	require.NoError(t, Write(filepath.Join(in, "a2.jwt"), []byte(atok)))
+
+	uc.Tags.Add("test")
+	utok, err = uc.Encode(akp)
+	require.NoError(t, err)
+	require.NoError(t, Write(filepath.Join(in, "u2.jwt"), []byte(utok)))
+
 	ofp := filepath.Join(ts.Dir, "out")
-	_, _, err = ExecuteCmd(createFixCmd(), "--in", in, "--out", ofp)
+	_, _, err = ExecuteCmd(createFixCmd(), "--creds", "--in", in, "--out", ofp)
 	require.NoError(t, err)
 
 	s, err := store.LoadStore(filepath.Join(ofp, "operators", "O"))
@@ -102,11 +121,17 @@ func Test_FixBasics(t *testing.T) {
 	require.NoError(t, err)
 	require.Equal(t, apk, aac.Subject)
 	require.Equal(t, "A", aac.Name)
+	require.True(t, aac.Tags.Contains("test"))
 
 	uuc, err := s.ReadUserClaim("A", "U")
 	require.NoError(t, err)
 	require.Equal(t, upk, uuc.Subject)
 	require.Equal(t, "U", uuc.Name)
+	require.True(t, uuc.Tags.Contains("test"))
+
+	uuc2, err := s.ReadUserClaim("A", "U2")
+	require.NoError(t, err)
+	require.Equal(t, upk2, uuc2.Subject)
 
 	okf := filepath.Join(ofp, "keys", "keys", opk[:1], opk[1:3], fmt.Sprintf("%s.nk", opk))
 	require.FileExists(t, okf)
diff --git a/cmd/generatediagram_test.go b/cmd/generatediagram_test.go
new file mode 100644
index 0000000..9a8800c
--- /dev/null
+++ b/cmd/generatediagram_test.go
@@ -0,0 +1,81 @@
+package cmd
+
+import (
+	"runtime"
+	"testing"
+
+	"github.com/nats-io/jwt/v2"
+	"github.com/stretchr/testify/require"
+)
+
+func buildDiagreamStore(t *testing.T, ts *TestStore) {
+	ts.AddAccount(t, "A")
+	ts.AddExport(t, "A", jwt.Stream, "a", 0, true)
+	ts.AddExport(t, "A", jwt.Service, "q", 0, true)
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	_, pub, _ := CreateAccountKey(t)
+	ac.SigningKeys.Add(pub)
+
+	ac.DefaultPermissions.Pub.Allow.Add("a.hello")
+	ac.DefaultPermissions.Pub.Deny.Add("a.bye")
+	ac.DefaultPermissions.Sub.Allow.Add("a.hi")
+	ac.DefaultPermissions.Sub.Allow.Add("a.goodbye")
+
+	token, err := ac.Encode(ts.OperatorKey)
+	require.NoError(t, err)
+	require.NoError(t, ts.Store.StoreRaw([]byte(token)))
+
+	ts.AddAccount(t, "B")
+	ts.AddImport(t, "A", jwt.Stream, "a", "B")
+	ts.AddImport(t, "A", jwt.Service, "q", "B")
+	ts.AddUser(t, "B", "b")
+
+	uc, err := ts.Store.ReadUserClaim("B", "b")
+	require.NoError(t, err)
+	uc.Permissions.Pub.Allow.Add("a.>")
+	uc.Permissions.Pub.Deny.Add("b.>")
+	uc.Permissions.Sub.Allow.Add("a.>")
+	uc.Permissions.Sub.Deny.Add("b.>")
+	token, err = uc.Encode(ts.GetAccountKey(t, "B"))
+	require.NoError(t, err)
+	require.NoError(t, ts.Store.StoreRaw([]byte(token)))
+}
+
+func Test_ObjectDiagram(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("running in windows - looking at output hangs")
+	}
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	buildDiagreamStore(t, ts)
+
+	stdOut, _, err := ExecuteCmd(createObjectDiagramCmd(), "--show-keys", "--users", "--detail")
+	require.NoError(t, err)
+	require.Contains(t, stdOut, "@startuml")
+	require.Contains(t, stdOut, "object \"O\" as")
+	require.Contains(t, stdOut, "object \"A\" as")
+	require.Contains(t, stdOut, "object \"B\" as")
+	require.Contains(t, stdOut, "object \"b\" as")
+	require.Contains(t, stdOut, "@enduml")
+}
+
+func Test_ComponentDiagram(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("running in windows - looking at output hangs")
+	}
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	buildDiagreamStore(t, ts)
+
+	stdOut, _, err := ExecuteCmd(createComponentDiagreamCmd(), "--detail")
+	require.NoError(t, err)
+
+	require.Contains(t, stdOut, "@startuml")
+	require.Contains(t, stdOut, "Component Diagram of Accounts - Operator O")
+	require.Contains(t, stdOut, "component [A]")
+	require.Contains(t, stdOut, "component [B]")
+	require.Contains(t, stdOut, "\"a\" << public stream >>")
+	require.Contains(t, stdOut, "\"q\" << public service >>")
+	require.Contains(t, stdOut, "@enduml")
+}
diff --git a/cmd/keys_test.go b/cmd/keys_test.go
index d004b3b..7d129e8 100644
--- a/cmd/keys_test.go
+++ b/cmd/keys_test.go
@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"runtime"
 	"testing"
 
 	"github.com/nats-io/nkeys"
@@ -98,6 +99,18 @@ func Test_HasOldStructure(t *testing.T) {
 	store.KeyStorePath = old
 }
 
+func Test_NoMigration(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("running in windows - looking at output hangs")
+	}
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	_, stdErr, err := ExecuteCmd(createMigrateKeysCmd())
+	require.NoError(t, err)
+	require.Contains(t, stdErr, "does not need migration")
+}
+
 func Test_MigrateKeys(t *testing.T) {
 	ts := NewEmptyStore(t)
 	defer ts.Done(t)
@@ -125,9 +138,8 @@ func Test_MigrateKeys(t *testing.T) {
 	require.NoError(t, err)
 	require.True(t, needsUpdate)
 
-	old, err := store.Migrate()
+	_, _, err = ExecuteCmd(createMigrateKeysCmd())
 	require.NoError(t, err)
-	require.DirExists(t, old)
 
 	// directory for keystore has a "keys" and "creds"
 	keysDir := filepath.Join(ks, "keys")
diff --git a/cmd/nkeyconfigbuilder_test.go b/cmd/nkeyconfigbuilder_test.go
index ed9d1c8..2c80698 100644
--- a/cmd/nkeyconfigbuilder_test.go
+++ b/cmd/nkeyconfigbuilder_test.go
@@ -133,8 +133,8 @@ func Test_NkeyResolverMapsImporter(t *testing.T) {
 
 	ts.AddAccount(t, "B")
 
-	ts.AddImport(t, "A", "service.b", "B")
-	ts.AddImport(t, "A", "stream.a", "B")
+	ts.AddImport(t, "A", jwt.Service, "service.b", "B")
+	ts.AddImport(t, "A", jwt.Stream, "stream.a", "B")
 
 	builder := NewNKeyConfigBuilder()
 
diff --git a/cmd/rtttool_test.go b/cmd/rtttool_test.go
new file mode 100644
index 0000000..78ae457
--- /dev/null
+++ b/cmd/rtttool_test.go
@@ -0,0 +1,51 @@
+package cmd
+
+import (
+	"github.com/nats-io/nats-server/v2/server"
+	"github.com/stretchr/testify/require"
+	"path/filepath"
+	"runtime"
+	"testing"
+	"time"
+)
+
+func Test_RttTool(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("running in windows - looking at output hangs")
+	}
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "SYS")
+
+	oc, err := ts.Store.ReadOperatorClaim()
+	require.NoError(t, err)
+	oc.SystemAccount = ts.GetAccountPublicKey(t, "SYS")
+	oc.OperatorServiceURLs.Add("nats://127.0.0.1:4222")
+	token, err := oc.Encode(ts.OperatorKey)
+	require.NoError(t, err)
+	require.NoError(t, ts.Store.StoreRaw([]byte(token)))
+
+	ts.AddAccount(t, "A")
+	ts.AddUser(t, "A", "a")
+
+	serverconf := filepath.Join(ts.Dir, "server.conf")
+	_, _, err = ExecuteCmd(createServerConfigCmd(), "--mem-resolver", "--config-file", serverconf)
+	require.NoError(t, err)
+
+	var opts server.Options
+	require.NoError(t, opts.ProcessConfigFile(serverconf))
+
+	s, err := server.NewServer(&opts)
+	require.NoError(t, err)
+
+	go s.Start()
+	if !s.ReadyForConnections(10 * time.Second) {
+		t.Fatal("Unable to start NATS Server in Go Routine")
+	}
+	defer s.Shutdown()
+
+	_, stdErr, err := ExecuteCmd(createToolRTTCmd(), "--account", "A", "--user", "a")
+	require.NoError(t, err)
+	require.Contains(t, stdErr, "round trip time to [nats://127.0.0.1:4222]")
+}
diff --git a/cmd/test_test.go b/cmd/test_test.go
new file mode 100644
index 0000000..fec377d
--- /dev/null
+++ b/cmd/test_test.go
@@ -0,0 +1,45 @@
+package cmd
+
+import (
+	"github.com/stretchr/testify/require"
+	"path/filepath"
+	"runtime"
+	"testing"
+)
+
+func Test_FlagTable(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("running in windows - looking at output hangs")
+	}
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	stdout, _, err := ExecuteCmd(GetRootCmd(), "test", "flags")
+	require.NoError(t, err)
+	require.Contains(t, stdout, "nsc validate")
+	require.Contains(t, stdout, "nsc add account")
+}
+
+func Test_WhoFlagTable(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("running in windows - looking at output hangs")
+	}
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	stdout, _, err := ExecuteCmd(GetRootCmd(), "test", "whoflag", "allow-pub")
+	require.NoError(t, err)
+	require.Contains(t, stdout, "nsc add user")
+}
+
+func Test_Doc(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	docs := filepath.Join(ts.Dir, "doc")
+	_, _, err := ExecuteCmd(GetRootCmd(), "test", "doc", docs)
+	require.NoError(t, err)
+	require.DirExists(t, docs)
+	require.FileExists(t, filepath.Join(docs, "nsc_add.md"))
+	require.FileExists(t, filepath.Join(docs, "nsc_validate.md"))
+}
diff --git a/cmd/util_test.go b/cmd/util_test.go
index 1115591..43c1090 100644
--- a/cmd/util_test.go
+++ b/cmd/util_test.go
@@ -340,8 +340,11 @@ func (ts *TestStore) ImportRequiresToken(t *testing.T, srcAccount string, subjec
 	return false
 }
 
-func (ts *TestStore) AddImport(t *testing.T, srcAccount string, subject string, targetAccountName string) {
+func (ts *TestStore) AddImport(t *testing.T, srcAccount string, kind jwt.ExportType, subject string, targetAccountName string) {
 	flags := []string{"--account", targetAccountName}
+	if kind == jwt.Service {
+		flags = append(flags, "--service")
+	}
 
 	if ts.ImportRequiresToken(t, srcAccount, subject) {
 		token := ts.GenerateActivation(t, srcAccount, subject, targetAccountName)
@@ -455,6 +458,11 @@ func StripTableDecorations(s string) string {
 	return re.ReplaceAllString(s, " ")
 }
 
+func StripMultipleSpaces(s string) string {
+	re := regexp.MustCompile(` +`)
+	return re.ReplaceAllString(s, " ")
+}
+
 func (ts *TestStore) GetAccountKey(t *testing.T, name string) nkeys.KeyPair {
 	ac, err := ts.Store.ReadAccountClaim(name)
 	require.NoError(t, err)