Skip to content

Commit

Permalink
test(context): add unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
phm07 committed May 10, 2024
1 parent 3c682ee commit 0f107da
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 0 deletions.
92 changes: 92 additions & 0 deletions internal/cmd/context/active_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package context_test

import (
"os"
"testing"

"github.com/stretchr/testify/assert"

"github.com/hetznercloud/cli/internal/cmd/context"
"github.com/hetznercloud/cli/internal/testutil"
)

func TestActive(t *testing.T) {

testConfig := `
active_context = "my-context"
[[contexts]]
name = "my-context"
token = "super secret token"
`

type testCase struct {
name string
args []string
config string
err string
expOut string
expErr string
preRun func()
postRun func()
}

testCases := []testCase{
{
name: "no arguments",
args: []string{},
config: testConfig,
expOut: "my-context\n",
},
{
name: "no config",
args: []string{},
},
{
name: "from env",
args: []string{},
config: testConfig,
preRun: func() {
_ = os.Setenv("HCLOUD_CONTEXT", "abcdef")
},
postRun: func() {
_ = os.Unsetenv("HCLOUD_CONTEXT")
},
// 'hcloud context active' outputs the active_context from the config, so it should not be affected by the env var
expOut: "my-context\n",
},
{
name: "invalid config",
args: []string{},
config: `active_context = "invalid-context-name"`,
// if there is no context with the name of the active_context, there should be no output
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {

if tt.preRun != nil {
tt.preRun()
}
if tt.postRun != nil {
defer tt.postRun()
}

fx := testutil.NewFixtureWithConfigFile(t, []byte(tt.config))
defer fx.Finish()

cmd := context.NewActiveCommand(fx.State())

out, errOut, err := fx.Run(cmd, tt.args)

if tt.err == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tt.err)
}
assert.Equal(t, tt.expErr, errOut)
assert.Equal(t, tt.expOut, out)
})
}
}
98 changes: 98 additions & 0 deletions internal/cmd/context/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package context_test

import (
"syscall"
"testing"

"github.com/stretchr/testify/assert"

"github.com/hetznercloud/cli/internal/cmd/context"
"github.com/hetznercloud/cli/internal/testutil"
)

func TestCreate(t *testing.T) {

testConfig := `
active_context = "my-context"
[[contexts]]
name = "my-context"
token = "super secret token"
`

type testCase struct {
name string
args []string
config string

Check failure on line 26 in internal/cmd/context/create_test.go

View workflow job for this annotation

GitHub Actions / lint

field `config` is unused (unused)
isTerm bool
token string
err string
expErr string
expOut string
}

testCases := []testCase{
{
name: "new context",
args: []string{"new-context"},
isTerm: true,
token: "q4acIB6pq2CwsPqF+dNR2B6NTrv4yxmsspvDC1a02OqfMQeCz7nOk4A3pcJha8ix",
expOut: `Token:
active_context = "new-context"
[[contexts]]
name = "my-context"
token = "super secret token"
[[contexts]]
name = "new-context"
token = "q4acIB6pq2CwsPqF+dNR2B6NTrv4yxmsspvDC1a02OqfMQeCz7nOk4A3pcJha8ix"
Context new-context created and activated
`,
},
{
name: "not terminal",
args: []string{"new-context"},
isTerm: false,
err: "context create is an interactive command",
expErr: "Error: context create is an interactive command\n",
},
{
name: "existing context",
args: []string{"my-context"},
isTerm: true,
token: "q4acIB6pq2CwsPqF+dNR2B6NTrv4yxmsspvDC1a02OqfMQeCz7nOk4A3pcJha8ix",
err: "name already used",
expErr: "Error: name already used\n",
},
{
name: "invalid name",
args: []string{""},
isTerm: true,
err: "invalid name",
expErr: "Error: invalid name\n",
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
fx := testutil.NewFixtureWithConfigFile(t, []byte(testConfig))
defer fx.Finish()

fx.Terminal.EXPECT().StdoutIsTerminal().Return(tt.isTerm)
fx.Terminal.EXPECT().ReadPassword(int(syscall.Stdin)).Return([]byte(tt.token), nil).AnyTimes()

cmd := context.NewCreateCommand(fx.State())

out, errOut, err := fx.Run(cmd, tt.args)

if tt.err == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tt.err)
}
assert.Equal(t, tt.expErr, errOut)
assert.Equal(t, tt.expOut, out)
})
}
}
8 changes: 8 additions & 0 deletions internal/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/hetznercloud/cli/internal/hcapi2"
"github.com/hetznercloud/cli/internal/state/config"
"github.com/hetznercloud/cli/internal/testutil/terminal"
"github.com/hetznercloud/cli/internal/version"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
)
Expand All @@ -18,19 +19,22 @@ type State interface {

Client() hcapi2.Client
Config() config.Config
Terminal() terminal.Terminal
}

type state struct {
context.Context

client hcapi2.Client
config config.Config
term terminal.Terminal
}

func New(cfg config.Config) (State, error) {
s := &state{
Context: context.Background(),
config: cfg,
term: terminal.DefaultTerminal{},
}

s.client = s.newClient()
Expand All @@ -45,6 +49,10 @@ func (c *state) Config() config.Config {
return c.config
}

func (c *state) Terminal() terminal.Terminal {
return c.term
}

func (c *state) newClient() hcapi2.Client {
var opts []hcloud.ClientOption

Expand Down
9 changes: 9 additions & 0 deletions internal/testutil/fixture.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
hcapi2_mock "github.com/hetznercloud/cli/internal/hcapi2/mock"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/cli/internal/state/config"
"github.com/hetznercloud/cli/internal/testutil/terminal"
)

// Fixture provides affordances for testing CLI commands.
Expand All @@ -22,6 +23,7 @@ type Fixture struct {
ActionWaiter *state.MockActionWaiter
TokenEnsurer *state.MockTokenEnsurer
Config *config.MockConfig
Terminal *terminal.MockTerminal
}

// NewFixture creates a new Fixture with default config file.
Expand All @@ -45,6 +47,7 @@ func NewFixtureWithConfigFile(t *testing.T, f any) *Fixture {
ActionWaiter: state.NewMockActionWaiter(ctrl),
TokenEnsurer: state.NewMockTokenEnsurer(ctrl),
Config: &config.MockConfig{Config: cfg},
Terminal: terminal.NewMockTerminal(ctrl),
}
}

Expand Down Expand Up @@ -79,6 +82,7 @@ type fixtureState struct {

client hcapi2.Client
config config.Config
term terminal.Terminal
}

func (*fixtureState) WriteConfig() error {
Expand All @@ -93,6 +97,10 @@ func (s *fixtureState) Config() config.Config {
return s.config
}

func (s *fixtureState) Terminal() terminal.Terminal {
return s.term
}

// State returns a state.State implementation for testing purposes.
func (f *Fixture) State() state.State {
return &fixtureState{
Expand All @@ -101,5 +109,6 @@ func (f *Fixture) State() state.State {
ActionWaiter: f.ActionWaiter,
client: f.Client,
config: f.Config,
term: f.Terminal,
}
}
26 changes: 26 additions & 0 deletions internal/testutil/terminal/terminal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package terminal

import (
"golang.org/x/term"

"github.com/hetznercloud/cli/internal/ui"
)

//go:generate go run github.com/golang/mock/mockgen -package terminal -destination zz_terminal_mock.go . Terminal

type Terminal interface {
StdoutIsTerminal() bool
ReadPassword(fd int) ([]byte, error)
}

type DefaultTerminal struct{}

func (DefaultTerminal) StdoutIsTerminal() bool {
return ui.StdoutIsTerminal()
}

func (DefaultTerminal) ReadPassword(fd int) ([]byte, error) {
return term.ReadPassword(fd)
}

var _ Terminal = DefaultTerminal{}
63 changes: 63 additions & 0 deletions internal/testutil/terminal/zz_terminal_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0f107da

Please sign in to comment.