Add tests for delete

This commit is contained in:
Mislav Marohnić 2021-09-22 16:11:34 +02:00
parent d2113e3b59
commit cb7b535b91
4 changed files with 425 additions and 7 deletions

View file

@ -27,10 +27,12 @@ type deleteOptions struct {
prompter prompter
}
//go:generate moq -fmt goimports -rm -out mock_prompter.go . prompter
type prompter interface {
Confirm(message string) (bool, error)
}
//go:generate moq -fmt goimports -rm -out mock_api.go . apiClient
type apiClient interface {
GetUser(ctx context.Context) (*api.User, error)
ListCodespaces(ctx context.Context, user string) ([]*api.Codespace, error)
@ -57,9 +59,9 @@ func newDeleteCmd() *cobra.Command {
},
}
deleteCmd.Flags().StringVarP(&opts.codespaceName, "codespace", "c", "", "Delete codespace by `name`")
deleteCmd.Flags().StringVarP(&opts.codespaceName, "codespace", "c", "", "The `name` of the codespace to delete")
deleteCmd.Flags().BoolVar(&opts.deleteAll, "all", false, "Delete all codespaces")
deleteCmd.Flags().StringVarP(&opts.repoFilter, "repo", "r", "", "Delete codespaces for a repository")
deleteCmd.Flags().StringVarP(&opts.repoFilter, "repo", "r", "", "Delete codespaces for a `repository`")
deleteCmd.Flags().BoolVarP(&opts.skipConfirm, "force", "f", false, "Skip confirmation for codespaces that contain unsaved changes")
deleteCmd.Flags().Uint16Var(&opts.keepDays, "days", 0, "Delete codespaces older than `N` days")
@ -116,6 +118,10 @@ func delete(ctx context.Context, opts deleteOptions) error {
codespacesToDelete = append(codespacesToDelete, c)
}
if len(codespacesToDelete) == 0 {
return errors.New("no codespaces to delete")
}
g := errgroup.Group{}
for _, c := range codespacesToDelete {
codespaceName := c.Name

View file

@ -2,28 +2,187 @@ package ghcs
import (
"context"
"fmt"
"sort"
"testing"
"time"
"github.com/github/ghcs/internal/api"
)
func TestDelete(t *testing.T) {
user := &api.User{Login: "hubot"}
now, _ := time.Parse(time.RFC3339, "2021-09-22T00:00:00Z")
daysAgo := func(n int) string {
return now.Add(time.Hour * -time.Duration(24*n)).Format(time.RFC3339)
}
tests := []struct {
name string
opts deleteOptions
wantErr bool
name string
opts deleteOptions
codespaces []*api.Codespace
confirms map[string]bool
wantErr bool
wantDeleted []string
}{
{
name: "by name",
opts: deleteOptions{
codespaceName: "foo-bar-123",
codespaceName: "hubot-robawt-abc",
},
codespaces: []*api.Codespace{
{
Name: "monalisa-spoonknife-123",
},
{
Name: "hubot-robawt-abc",
},
},
wantDeleted: []string{"hubot-robawt-abc"},
},
{
name: "by repo",
opts: deleteOptions{
repoFilter: "monalisa/spoon-knife",
},
codespaces: []*api.Codespace{
{
Name: "monalisa-spoonknife-123",
RepositoryNWO: "monalisa/Spoon-Knife",
},
{
Name: "hubot-robawt-abc",
RepositoryNWO: "hubot/ROBAWT",
},
{
Name: "monalisa-spoonknife-c4f3",
RepositoryNWO: "monalisa/Spoon-Knife",
},
},
wantDeleted: []string{"monalisa-spoonknife-123", "monalisa-spoonknife-c4f3"},
},
{
name: "unused",
opts: deleteOptions{
deleteAll: true,
keepDays: 3,
},
codespaces: []*api.Codespace{
{
Name: "monalisa-spoonknife-123",
LastUsedAt: daysAgo(1),
},
{
Name: "hubot-robawt-abc",
LastUsedAt: daysAgo(4),
},
{
Name: "monalisa-spoonknife-c4f3",
LastUsedAt: daysAgo(10),
},
},
wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-c4f3"},
},
{
name: "with confirm",
opts: deleteOptions{
isInteractive: true,
deleteAll: true,
skipConfirm: false,
},
codespaces: []*api.Codespace{
{
Name: "monalisa-spoonknife-123",
Environment: api.CodespaceEnvironment{
GitStatus: api.CodespaceEnvironmentGitStatus{
HasUnpushedChanges: true,
},
},
},
{
Name: "hubot-robawt-abc",
Environment: api.CodespaceEnvironment{
GitStatus: api.CodespaceEnvironmentGitStatus{
HasUncommitedChanges: true,
},
},
},
{
Name: "monalisa-spoonknife-c4f3",
Environment: api.CodespaceEnvironment{
GitStatus: api.CodespaceEnvironmentGitStatus{
HasUnpushedChanges: false,
HasUncommitedChanges: false,
},
},
},
},
confirms: map[string]bool{
"Codespace monalisa-spoonknife-123 has unsaved changes. OK to delete?": false,
"Codespace hubot-robawt-abc has unsaved changes. OK to delete?": true,
},
wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-c4f3"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := delete(context.Background(), tt.opts)
apiMock := &apiClientMock{
GetUserFunc: func(_ context.Context) (*api.User, error) {
return user, nil
},
ListCodespacesFunc: func(_ context.Context, userLogin string) ([]*api.Codespace, error) {
if userLogin != user.Login {
return nil, fmt.Errorf("unexpected user %q", userLogin)
}
return tt.codespaces, nil
},
DeleteCodespaceFunc: func(_ context.Context, userLogin, name string) error {
if userLogin != user.Login {
return fmt.Errorf("unexpected user %q", userLogin)
}
return nil
},
}
opts := tt.opts
opts.apiClient = apiMock
opts.now = func() time.Time { return now }
opts.prompter = &prompterMock{
ConfirmFunc: func(msg string) (bool, error) {
res, found := tt.confirms[msg]
if !found {
return false, fmt.Errorf("unexpected prompt %q", msg)
}
return res, nil
},
}
err := delete(context.Background(), opts)
if (err != nil) != tt.wantErr {
t.Errorf("delete() error = %v, wantErr %v", err, tt.wantErr)
}
if n := len(apiMock.GetUserCalls()); n != 1 {
t.Errorf("GetUser invoked %d times, expected %d", n, 1)
}
var gotDeleted []string
for _, delArgs := range apiMock.DeleteCodespaceCalls() {
gotDeleted = append(gotDeleted, delArgs.Name)
}
sort.Strings(gotDeleted)
if !sliceEquals(gotDeleted, tt.wantDeleted) {
t.Errorf("deleted %q, want %q", gotDeleted, tt.wantDeleted)
}
})
}
}
func sliceEquals(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

180
cmd/ghcs/mock_api.go Normal file
View file

@ -0,0 +1,180 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package ghcs
import (
"context"
"sync"
"github.com/github/ghcs/internal/api"
)
// Ensure, that apiClientMock does implement apiClient.
// If this is not the case, regenerate this file with moq.
var _ apiClient = &apiClientMock{}
// apiClientMock is a mock implementation of apiClient.
//
// func TestSomethingThatUsesapiClient(t *testing.T) {
//
// // make and configure a mocked apiClient
// mockedapiClient := &apiClientMock{
// DeleteCodespaceFunc: func(ctx context.Context, user string, name string) error {
// panic("mock out the DeleteCodespace method")
// },
// GetUserFunc: func(ctx context.Context) (*api.User, error) {
// panic("mock out the GetUser method")
// },
// ListCodespacesFunc: func(ctx context.Context, user string) ([]*api.Codespace, error) {
// panic("mock out the ListCodespaces method")
// },
// }
//
// // use mockedapiClient in code that requires apiClient
// // and then make assertions.
//
// }
type apiClientMock struct {
// DeleteCodespaceFunc mocks the DeleteCodespace method.
DeleteCodespaceFunc func(ctx context.Context, user string, name string) error
// GetUserFunc mocks the GetUser method.
GetUserFunc func(ctx context.Context) (*api.User, error)
// ListCodespacesFunc mocks the ListCodespaces method.
ListCodespacesFunc func(ctx context.Context, user string) ([]*api.Codespace, error)
// calls tracks calls to the methods.
calls struct {
// DeleteCodespace holds details about calls to the DeleteCodespace method.
DeleteCodespace []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// User is the user argument value.
User string
// Name is the name argument value.
Name string
}
// GetUser holds details about calls to the GetUser method.
GetUser []struct {
// Ctx is the ctx argument value.
Ctx context.Context
}
// ListCodespaces holds details about calls to the ListCodespaces method.
ListCodespaces []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// User is the user argument value.
User string
}
}
lockDeleteCodespace sync.RWMutex
lockGetUser sync.RWMutex
lockListCodespaces sync.RWMutex
}
// DeleteCodespace calls DeleteCodespaceFunc.
func (mock *apiClientMock) DeleteCodespace(ctx context.Context, user string, name string) error {
if mock.DeleteCodespaceFunc == nil {
panic("apiClientMock.DeleteCodespaceFunc: method is nil but apiClient.DeleteCodespace was just called")
}
callInfo := struct {
Ctx context.Context
User string
Name string
}{
Ctx: ctx,
User: user,
Name: name,
}
mock.lockDeleteCodespace.Lock()
mock.calls.DeleteCodespace = append(mock.calls.DeleteCodespace, callInfo)
mock.lockDeleteCodespace.Unlock()
return mock.DeleteCodespaceFunc(ctx, user, name)
}
// DeleteCodespaceCalls gets all the calls that were made to DeleteCodespace.
// Check the length with:
// len(mockedapiClient.DeleteCodespaceCalls())
func (mock *apiClientMock) DeleteCodespaceCalls() []struct {
Ctx context.Context
User string
Name string
} {
var calls []struct {
Ctx context.Context
User string
Name string
}
mock.lockDeleteCodespace.RLock()
calls = mock.calls.DeleteCodespace
mock.lockDeleteCodespace.RUnlock()
return calls
}
// GetUser calls GetUserFunc.
func (mock *apiClientMock) GetUser(ctx context.Context) (*api.User, error) {
if mock.GetUserFunc == nil {
panic("apiClientMock.GetUserFunc: method is nil but apiClient.GetUser was just called")
}
callInfo := struct {
Ctx context.Context
}{
Ctx: ctx,
}
mock.lockGetUser.Lock()
mock.calls.GetUser = append(mock.calls.GetUser, callInfo)
mock.lockGetUser.Unlock()
return mock.GetUserFunc(ctx)
}
// GetUserCalls gets all the calls that were made to GetUser.
// Check the length with:
// len(mockedapiClient.GetUserCalls())
func (mock *apiClientMock) GetUserCalls() []struct {
Ctx context.Context
} {
var calls []struct {
Ctx context.Context
}
mock.lockGetUser.RLock()
calls = mock.calls.GetUser
mock.lockGetUser.RUnlock()
return calls
}
// ListCodespaces calls ListCodespacesFunc.
func (mock *apiClientMock) ListCodespaces(ctx context.Context, user string) ([]*api.Codespace, error) {
if mock.ListCodespacesFunc == nil {
panic("apiClientMock.ListCodespacesFunc: method is nil but apiClient.ListCodespaces was just called")
}
callInfo := struct {
Ctx context.Context
User string
}{
Ctx: ctx,
User: user,
}
mock.lockListCodespaces.Lock()
mock.calls.ListCodespaces = append(mock.calls.ListCodespaces, callInfo)
mock.lockListCodespaces.Unlock()
return mock.ListCodespacesFunc(ctx, user)
}
// ListCodespacesCalls gets all the calls that were made to ListCodespaces.
// Check the length with:
// len(mockedapiClient.ListCodespacesCalls())
func (mock *apiClientMock) ListCodespacesCalls() []struct {
Ctx context.Context
User string
} {
var calls []struct {
Ctx context.Context
User string
}
mock.lockListCodespaces.RLock()
calls = mock.calls.ListCodespaces
mock.lockListCodespaces.RUnlock()
return calls
}

73
cmd/ghcs/mock_prompter.go Normal file
View file

@ -0,0 +1,73 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package ghcs
import (
"sync"
)
// Ensure, that prompterMock does implement prompter.
// If this is not the case, regenerate this file with moq.
var _ prompter = &prompterMock{}
// prompterMock is a mock implementation of prompter.
//
// func TestSomethingThatUsesprompter(t *testing.T) {
//
// // make and configure a mocked prompter
// mockedprompter := &prompterMock{
// ConfirmFunc: func(message string) (bool, error) {
// panic("mock out the Confirm method")
// },
// }
//
// // use mockedprompter in code that requires prompter
// // and then make assertions.
//
// }
type prompterMock struct {
// ConfirmFunc mocks the Confirm method.
ConfirmFunc func(message string) (bool, error)
// calls tracks calls to the methods.
calls struct {
// Confirm holds details about calls to the Confirm method.
Confirm []struct {
// Message is the message argument value.
Message string
}
}
lockConfirm sync.RWMutex
}
// Confirm calls ConfirmFunc.
func (mock *prompterMock) Confirm(message string) (bool, error) {
if mock.ConfirmFunc == nil {
panic("prompterMock.ConfirmFunc: method is nil but prompter.Confirm was just called")
}
callInfo := struct {
Message string
}{
Message: message,
}
mock.lockConfirm.Lock()
mock.calls.Confirm = append(mock.calls.Confirm, callInfo)
mock.lockConfirm.Unlock()
return mock.ConfirmFunc(message)
}
// ConfirmCalls gets all the calls that were made to Confirm.
// Check the length with:
// len(mockedprompter.ConfirmCalls())
func (mock *prompterMock) ConfirmCalls() []struct {
Message string
} {
var calls []struct {
Message string
}
mock.lockConfirm.RLock()
calls = mock.calls.Confirm
mock.lockConfirm.RUnlock()
return calls
}