Previously, deleting caches by key or key+ref could misreport the number of deleted entries, especially when multiple caches matched the criteria. This change ensures the actual number of deleted caches is reported by consuming the API response's total_count field.
412 lines
12 KiB
Go
412 lines
12 KiB
Go
package delete
|
||
|
||
import (
|
||
"bytes"
|
||
"net/http"
|
||
"net/url"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/cli/cli/v2/internal/ghrepo"
|
||
"github.com/cli/cli/v2/pkg/cmd/cache/shared"
|
||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||
"github.com/cli/cli/v2/pkg/httpmock"
|
||
"github.com/cli/cli/v2/pkg/iostreams"
|
||
"github.com/google/shlex"
|
||
"github.com/stretchr/testify/assert"
|
||
)
|
||
|
||
func TestNewCmdDelete(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
cli string
|
||
wants DeleteOptions
|
||
wantsErr string
|
||
}{
|
||
{
|
||
name: "no arguments",
|
||
cli: "",
|
||
wantsErr: "must provide either cache id, cache key, or use --all",
|
||
},
|
||
{
|
||
name: "id argument",
|
||
cli: "123",
|
||
wants: DeleteOptions{Identifier: "123"},
|
||
},
|
||
{
|
||
name: "key argument",
|
||
cli: "A-Cache-Key",
|
||
wants: DeleteOptions{Identifier: "A-Cache-Key"},
|
||
},
|
||
{
|
||
name: "delete all flag",
|
||
cli: "--all",
|
||
wants: DeleteOptions{DeleteAll: true},
|
||
},
|
||
{
|
||
name: "delete all and succeed-on-no-caches flags",
|
||
cli: "--all --succeed-on-no-caches",
|
||
wants: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true},
|
||
},
|
||
{
|
||
name: "succeed-on-no-caches flag",
|
||
cli: "--succeed-on-no-caches",
|
||
wantsErr: "--succeed-on-no-caches must be used in conjunction with --all",
|
||
},
|
||
{
|
||
name: "succeed-on-no-caches flag and id argument",
|
||
cli: "--succeed-on-no-caches 123",
|
||
wantsErr: "--succeed-on-no-caches must be used in conjunction with --all",
|
||
},
|
||
{
|
||
name: "key argument and delete all flag",
|
||
cli: "cache-key --all",
|
||
wantsErr: "specify only one of cache id, cache key, or --all",
|
||
},
|
||
{
|
||
name: "id argument and delete all flag",
|
||
cli: "1 --all",
|
||
wantsErr: "specify only one of cache id, cache key, or --all",
|
||
},
|
||
{
|
||
name: "key argument with ref",
|
||
cli: "cache-key --ref refs/heads/main",
|
||
wants: DeleteOptions{Identifier: "cache-key", Ref: "refs/heads/main"},
|
||
},
|
||
{
|
||
name: "ref flag without cache key",
|
||
cli: "--ref refs/heads/main",
|
||
wantsErr: "must provide a cache key",
|
||
},
|
||
{
|
||
name: "ref flag with cache id",
|
||
cli: "123 --ref refs/heads/main",
|
||
wantsErr: "--ref cannot be used with cache ID",
|
||
},
|
||
{
|
||
name: "ref flag with all flag",
|
||
cli: "--all --ref refs/heads/main",
|
||
wantsErr: "--ref cannot be used with --all",
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
f := &cmdutil.Factory{}
|
||
argv, err := shlex.Split(tt.cli)
|
||
assert.NoError(t, err)
|
||
var gotOpts *DeleteOptions
|
||
cmd := NewCmdDelete(f, func(opts *DeleteOptions) error {
|
||
gotOpts = opts
|
||
return nil
|
||
})
|
||
cmd.SetArgs(argv)
|
||
cmd.SetIn(&bytes.Buffer{})
|
||
cmd.SetOut(&bytes.Buffer{})
|
||
cmd.SetErr(&bytes.Buffer{})
|
||
|
||
_, err = cmd.ExecuteC()
|
||
if tt.wantsErr != "" {
|
||
assert.EqualError(t, err, tt.wantsErr)
|
||
return
|
||
}
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, tt.wants.DeleteAll, gotOpts.DeleteAll)
|
||
assert.Equal(t, tt.wants.SucceedOnNoCaches, gotOpts.SucceedOnNoCaches)
|
||
assert.Equal(t, tt.wants.Identifier, gotOpts.Identifier)
|
||
assert.Equal(t, tt.wants.Ref, gotOpts.Ref)
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestDeleteRun(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
opts DeleteOptions
|
||
stubs func(*httpmock.Registry)
|
||
tty bool
|
||
wantErr bool
|
||
wantErrMsg string
|
||
wantStderr string
|
||
wantStdout string
|
||
}{
|
||
{
|
||
name: "deletes cache tty",
|
||
opts: DeleteOptions{Identifier: "123"},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||
httpmock.StatusStringResponse(204, ""),
|
||
)
|
||
},
|
||
tty: true,
|
||
wantStdout: "✓ Deleted 1 cache from OWNER/REPO\n",
|
||
},
|
||
{
|
||
name: "deletes cache notty",
|
||
opts: DeleteOptions{Identifier: "123"},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||
httpmock.StatusStringResponse(204, ""),
|
||
)
|
||
},
|
||
tty: false,
|
||
wantStdout: "",
|
||
},
|
||
{
|
||
name: "non-existent cache",
|
||
opts: DeleteOptions{Identifier: "123"},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||
httpmock.StatusStringResponse(404, ""),
|
||
)
|
||
},
|
||
wantErr: true,
|
||
wantErrMsg: "X Could not find a cache matching 123 in OWNER/REPO",
|
||
},
|
||
{
|
||
name: "deletes all caches",
|
||
opts: DeleteOptions{DeleteAll: true},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||
httpmock.JSONResponse(shared.CachePayload{
|
||
ActionsCaches: []shared.Cache{
|
||
{
|
||
Id: 123,
|
||
Key: "foo",
|
||
CreatedAt: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
|
||
LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
||
},
|
||
{
|
||
Id: 456,
|
||
Key: "bar",
|
||
CreatedAt: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
|
||
LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
||
},
|
||
},
|
||
TotalCount: 2,
|
||
}),
|
||
)
|
||
reg.Register(
|
||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||
httpmock.StatusStringResponse(204, ""),
|
||
)
|
||
reg.Register(
|
||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/456"),
|
||
httpmock.StatusStringResponse(204, ""),
|
||
)
|
||
},
|
||
tty: true,
|
||
wantStdout: "✓ Deleted 2 caches from OWNER/REPO\n",
|
||
},
|
||
{
|
||
name: "attempts to delete all caches but api errors",
|
||
opts: DeleteOptions{DeleteAll: true},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||
httpmock.StatusStringResponse(500, ""),
|
||
)
|
||
},
|
||
tty: true,
|
||
wantErr: true,
|
||
wantErrMsg: "HTTP 500 (https://api.github.com/repos/OWNER/REPO/actions/caches?per_page=100)",
|
||
},
|
||
{
|
||
name: "displays delete error",
|
||
opts: DeleteOptions{Identifier: "123"},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||
httpmock.StatusStringResponse(500, ""),
|
||
)
|
||
},
|
||
wantErr: true,
|
||
wantErrMsg: "X Failed to delete cache: HTTP 500 (https://api.github.com/repos/OWNER/REPO/actions/caches/123)",
|
||
},
|
||
{
|
||
name: "keys must be percent-encoded before being used as query params",
|
||
opts: DeleteOptions{Identifier: "a weird_cache+key"},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
|
||
"key": []string{"a weird_cache+key"},
|
||
}),
|
||
httpmock.JSONResponse(shared.CachePayload{
|
||
TotalCount: 1,
|
||
}),
|
||
)
|
||
},
|
||
tty: true,
|
||
wantStdout: "✓ Deleted 1 cache from OWNER/REPO\n",
|
||
},
|
||
{
|
||
name: "deletes multiple caches by key",
|
||
opts: DeleteOptions{Identifier: "shared-cache-key"},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
|
||
"key": []string{"shared-cache-key"},
|
||
}),
|
||
httpmock.JSONResponse(shared.CachePayload{
|
||
TotalCount: 5,
|
||
}),
|
||
)
|
||
},
|
||
tty: true,
|
||
wantStdout: "✓ Deleted 5 caches from OWNER/REPO\n",
|
||
},
|
||
{
|
||
name: "no caches to delete when deleting all",
|
||
opts: DeleteOptions{DeleteAll: true},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||
httpmock.JSONResponse(shared.CachePayload{
|
||
ActionsCaches: []shared.Cache{},
|
||
TotalCount: 0,
|
||
}),
|
||
)
|
||
},
|
||
tty: false,
|
||
wantErr: true,
|
||
wantErrMsg: "X No caches to delete",
|
||
},
|
||
{
|
||
name: "no caches to delete when deleting all but succeed on no cache tty",
|
||
opts: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||
httpmock.JSONResponse(shared.CachePayload{
|
||
ActionsCaches: []shared.Cache{},
|
||
TotalCount: 0,
|
||
}),
|
||
)
|
||
},
|
||
tty: true,
|
||
wantErr: false,
|
||
wantStdout: "✓ No caches to delete\n",
|
||
},
|
||
{
|
||
name: "no caches to delete when deleting all but succeed on no cache non-tty",
|
||
opts: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||
httpmock.JSONResponse(shared.CachePayload{
|
||
ActionsCaches: []shared.Cache{},
|
||
TotalCount: 0,
|
||
}),
|
||
)
|
||
},
|
||
tty: false,
|
||
wantErr: false,
|
||
wantStdout: "",
|
||
},
|
||
{
|
||
name: "deletes cache with ref tty",
|
||
opts: DeleteOptions{Identifier: "cache-key", Ref: "refs/heads/main"},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
|
||
"key": []string{"cache-key"},
|
||
"ref": []string{"refs/heads/main"},
|
||
}),
|
||
httpmock.JSONResponse(shared.CachePayload{
|
||
TotalCount: 1,
|
||
}),
|
||
)
|
||
},
|
||
tty: true,
|
||
wantStdout: "✓ Deleted 1 cache from OWNER/REPO\n",
|
||
},
|
||
{
|
||
name: "deletes cache with ref non-tty",
|
||
opts: DeleteOptions{Identifier: "cache-key", Ref: "refs/heads/main"},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
|
||
"key": []string{"cache-key"},
|
||
"ref": []string{"refs/heads/main"},
|
||
}),
|
||
httpmock.JSONResponse(shared.CachePayload{
|
||
TotalCount: 1,
|
||
}),
|
||
)
|
||
},
|
||
tty: false,
|
||
wantStdout: "",
|
||
},
|
||
{
|
||
name: "deletes multiple caches by key and ref",
|
||
opts: DeleteOptions{Identifier: "cache-key", Ref: "refs/heads/feature"},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
|
||
"key": []string{"cache-key"},
|
||
"ref": []string{"refs/heads/feature"},
|
||
}),
|
||
httpmock.JSONResponse(shared.CachePayload{
|
||
TotalCount: 3,
|
||
}),
|
||
)
|
||
},
|
||
tty: true,
|
||
wantStdout: "✓ Deleted 3 caches from OWNER/REPO\n",
|
||
},
|
||
{
|
||
// As of now, the API returns HTTP 404 for invalid or non-existent refs.
|
||
name: "cache key exists but ref is invalid/not-found",
|
||
opts: DeleteOptions{Identifier: "existing-cache-key", Ref: "invalid-ref"},
|
||
stubs: func(reg *httpmock.Registry) {
|
||
reg.Register(
|
||
httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
|
||
"key": []string{"existing-cache-key"},
|
||
"ref": []string{"invalid-ref"},
|
||
}),
|
||
httpmock.StatusStringResponse(404, ""),
|
||
)
|
||
},
|
||
wantErr: true,
|
||
wantErrMsg: "X Could not find a cache matching existing-cache-key (with ref invalid-ref) in OWNER/REPO",
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
reg := &httpmock.Registry{}
|
||
if tt.stubs != nil {
|
||
tt.stubs(reg)
|
||
}
|
||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||
return &http.Client{Transport: reg}, nil
|
||
}
|
||
ios, _, stdout, stderr := iostreams.Test()
|
||
ios.SetStdoutTTY(tt.tty)
|
||
ios.SetStdinTTY(tt.tty)
|
||
ios.SetStderrTTY(tt.tty)
|
||
tt.opts.IO = ios
|
||
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
|
||
return ghrepo.New("OWNER", "REPO"), nil
|
||
}
|
||
defer reg.Verify(t)
|
||
|
||
err := deleteRun(&tt.opts)
|
||
if tt.wantErr {
|
||
if tt.wantErrMsg != "" {
|
||
assert.EqualError(t, err, tt.wantErrMsg)
|
||
} else {
|
||
assert.Error(t, err)
|
||
}
|
||
} else {
|
||
assert.NoError(t, err)
|
||
}
|
||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||
})
|
||
}
|
||
}
|