Add tests for delete
This commit is contained in:
parent
d2113e3b59
commit
cb7b535b91
4 changed files with 425 additions and 7 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
180
cmd/ghcs/mock_api.go
Normal 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
73
cmd/ghcs/mock_prompter.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue