1/*
2Copyright 2018 The Doctl Authors All rights reserved.
3Licensed under the Apache License, Version 2.0 (the "License");
4you may not use this file except in compliance with the License.
5You may obtain a copy of the License at
6    http://www.apache.org/licenses/LICENSE-2.0
7Unless required by applicable law or agreed to in writing, software
8distributed under the License is distributed on an "AS IS" BASIS,
9WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10See the License for the specific language governing permissions and
11limitations under the License.
12*/
13
14package commands
15
16import (
17	"bytes"
18	"errors"
19	"fmt"
20	"os"
21	"strings"
22	"testing"
23	"time"
24
25	"github.com/digitalocean/doctl"
26	"github.com/digitalocean/doctl/do"
27	"github.com/digitalocean/doctl/do/mocks"
28	"github.com/digitalocean/godo"
29	"github.com/golang/mock/gomock"
30	"github.com/stretchr/testify/assert"
31	k8sapiv1 "k8s.io/api/core/v1"
32	k8sscheme "k8s.io/client-go/kubernetes/scheme"
33)
34
35var (
36	testRegistryName     = "container-registry"
37	testSubscriptionTier = "basic"
38	invalidRegistryName  = "invalid-container-registry"
39	testRegistry         = do.Registry{Registry: &godo.Registry{Name: testRegistryName}}
40	testRepoName         = "test-repository"
41	testRepositoryTag    = do.RepositoryTag{
42		RepositoryTag: &godo.RepositoryTag{
43			RegistryName:        testRegistryName,
44			Repository:          testRepoName,
45			Tag:                 "tag",
46			ManifestDigest:      "sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b",
47			CompressedSizeBytes: 50,
48			SizeBytes:           100,
49			UpdatedAt:           time.Now(),
50		},
51	}
52	testRepositoryManifest = do.RepositoryManifest{
53		RepositoryManifest: &godo.RepositoryManifest{
54			RegistryName:        testRegistryName,
55			Repository:          testRepoName,
56			Digest:              "sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b",
57			CompressedSizeBytes: 50,
58			SizeBytes:           100,
59			UpdatedAt:           time.Now(),
60			Tags:                []string{"v1", "v2"},
61			Blobs: []*godo.Blob{
62				{
63					Digest:              "sha256:123",
64					CompressedSizeBytes: 123,
65				},
66				{
67					Digest:              "sha256:456",
68					CompressedSizeBytes: 456,
69				},
70			},
71		},
72	}
73	testRepositoryManifestNoTags = do.RepositoryManifest{
74		RepositoryManifest: &godo.RepositoryManifest{
75			RegistryName:        testRegistryName,
76			Repository:          testRepoName,
77			Digest:              "sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b",
78			CompressedSizeBytes: 50,
79			SizeBytes:           100,
80			UpdatedAt:           time.Now(),
81			Tags:                []string{ /* I don't need any tags! */ },
82			Blobs: []*godo.Blob{
83				{
84					Digest:              "sha256:123",
85					CompressedSizeBytes: 123,
86				},
87				{
88					Digest:              "sha256:456",
89					CompressedSizeBytes: 456,
90				},
91			},
92		},
93	}
94	testRepository = do.Repository{
95		Repository: &godo.Repository{
96			RegistryName: testRegistryName,
97			Name:         testRegistryName,
98			TagCount:     5,
99			LatestTag:    testRepositoryTag.RepositoryTag,
100		},
101	}
102	testRepositoryV2 = do.RepositoryV2{
103		RepositoryV2: &godo.RepositoryV2{
104			RegistryName:   testRegistryName,
105			Name:           testRegistryName,
106			TagCount:       2,
107			ManifestCount:  1,
108			LatestManifest: testRepositoryManifest.RepositoryManifest,
109		},
110	}
111	testRepositoryV2NoTags = do.RepositoryV2{
112		RepositoryV2: &godo.RepositoryV2{
113			RegistryName:   testRegistryName,
114			Name:           testRegistryName,
115			TagCount:       0,
116			ManifestCount:  1,
117			LatestManifest: testRepositoryManifestNoTags.RepositoryManifest,
118		},
119	}
120	testDockerCredentials = &godo.DockerCredentials{
121		// the base64 string is "username:password"
122		DockerConfigJSON: []byte(`{"auths":{"hostname":{"auth":"dXNlcm5hbWU6cGFzc3dvcmQ="}}}`),
123	}
124	testGCBlobsDeleted    = uint64(42)
125	testGCFreedBytes      = uint64(666)
126	testGCStatus          = "requested"
127	testGCUUID            = "gc-uuid"
128	invalidGCUUID         = "invalid-gc-uuid"
129	testTime              = time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC)
130	testGarbageCollection = &do.GarbageCollection{
131		&godo.GarbageCollection{
132			UUID:         testGCUUID,
133			RegistryName: testRegistryName,
134			Status:       testGCStatus,
135			CreatedAt:    testTime,
136			UpdatedAt:    testTime,
137			BlobsDeleted: testGCBlobsDeleted,
138			FreedBytes:   testGCFreedBytes,
139		},
140	}
141)
142
143func TestRegistryCommand(t *testing.T) {
144	cmd := Registry()
145	assert.NotNil(t, cmd)
146	assertCommandNames(t, cmd, "create", "get", "delete", "login", "logout", "options", "kubernetes-manifest", "repository", "docker-config", "garbage-collection")
147}
148
149func TestRepositoryCommand(t *testing.T) {
150	cmd := Repository()
151	assert.NotNil(t, cmd)
152	assertCommandNames(t, cmd, "list", "list-v2", "list-manifests", "list-tags", "delete-manifest", "delete-tag")
153}
154
155func TestGarbageCollectionCommand(t *testing.T) {
156	cmd := GarbageCollection()
157	assert.NotNil(t, cmd)
158	assertCommandNames(t, cmd, "get-active", "start", "cancel", "list")
159}
160
161func TestRegistryCreate(t *testing.T) {
162	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
163		rcr := godo.RegistryCreateRequest{
164			Name:                 testRegistryName,
165			SubscriptionTierSlug: testSubscriptionTier,
166		}
167		tm.registry.EXPECT().Create(&rcr).Return(&testRegistry, nil)
168		config.Args = append(config.Args, testRegistryName)
169		config.Doit.Set(config.NS, doctl.ArgSubscriptionTier, "basic")
170
171		err := RunRegistryCreate(config)
172		assert.NoError(t, err)
173	})
174}
175
176func TestRegistryGet(t *testing.T) {
177	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
178		tm.registry.EXPECT().Get().Return(&testRegistry, nil)
179
180		err := RunRegistryGet(config)
181		assert.NoError(t, err)
182	})
183}
184
185func TestRegistryDelete(t *testing.T) {
186	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
187		tm.registry.EXPECT().Delete().Return(nil)
188
189		config.Doit.Set(config.NS, doctl.ArgForce, true)
190
191		err := RunRegistryDelete(config)
192		assert.NoError(t, err)
193	})
194}
195
196func TestDockerConfig(t *testing.T) {
197	tests := []struct {
198		name          string
199		readWrite     bool
200		expirySeconds int
201		expect        func(m *mocks.MockRegistryService)
202	}{
203		{
204			name:          "read-only-no-expiry",
205			readWrite:     false,
206			expirySeconds: 0,
207			expect: func(m *mocks.MockRegistryService) {
208				m.EXPECT().DockerCredentials(&godo.RegistryDockerCredentialsRequest{
209					ReadWrite: false,
210				}).Return(testDockerCredentials, nil)
211			},
212		},
213		{
214			name:          "read-write-no-expiry",
215			readWrite:     true,
216			expirySeconds: 0,
217			expect: func(m *mocks.MockRegistryService) {
218				m.EXPECT().DockerCredentials(&godo.RegistryDockerCredentialsRequest{
219					ReadWrite: true,
220				}).Return(testDockerCredentials, nil)
221			},
222		},
223		{
224			name:          "read-only-with-expiry",
225			readWrite:     false,
226			expirySeconds: 3600,
227			expect: func(m *mocks.MockRegistryService) {
228				m.EXPECT().DockerCredentials(&godo.RegistryDockerCredentialsRequest{
229					ReadWrite:     false,
230					ExpirySeconds: godo.Int(3600),
231				}).Return(testDockerCredentials, nil)
232			},
233		},
234		{
235			name:          "read-write-with-expiry",
236			readWrite:     true,
237			expirySeconds: 3600,
238			expect: func(m *mocks.MockRegistryService) {
239				m.EXPECT().DockerCredentials(&godo.RegistryDockerCredentialsRequest{
240					ReadWrite:     true,
241					ExpirySeconds: godo.Int(3600),
242				}).Return(testDockerCredentials, nil)
243			},
244		},
245	}
246
247	for _, test := range tests {
248		t.Run(test.name, func(t *testing.T) {
249			withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
250				if test.expect != nil {
251					test.expect(tm.registry)
252				}
253
254				config.Doit.Set(config.NS, doctl.ArgReadWrite, test.readWrite)
255				config.Doit.Set(config.NS, doctl.ArgRegistryExpirySeconds, test.expirySeconds)
256
257				var output bytes.Buffer
258				config.Out = &output
259
260				err := RunDockerConfig(config)
261				assert.NoError(t, err)
262
263				expectedOutput := append(testDockerCredentials.DockerConfigJSON, '\n')
264				assert.Equal(t, expectedOutput, output.Bytes())
265			})
266		})
267	}
268}
269
270func TestRepositoryList(t *testing.T) {
271	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
272		tm.registry.EXPECT().Get().Return(&testRegistry, nil)
273		tm.registry.EXPECT().ListRepositories(testRepository.RegistryName).Return([]do.Repository{testRepository}, nil)
274
275		err := RunListRepositories(config)
276		assert.NoError(t, err)
277	})
278}
279
280func TestRepositoryListV2(t *testing.T) {
281	t.Run("with latest tag", func(t *testing.T) {
282		withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
283			tm.registry.EXPECT().Get().Return(&testRegistry, nil)
284			tm.registry.EXPECT().ListRepositoriesV2(testRepositoryV2.RegistryName).Return([]do.RepositoryV2{testRepositoryV2}, nil)
285
286			var buf bytes.Buffer
287			config.Out = &buf
288			err := RunListRepositoriesV2(config)
289			assert.NoError(t, err)
290
291			output := buf.String()
292			// instead of trying to match the output, do some basic content checks
293			assert.True(t, strings.Contains(output, testRepositoryV2.Name))
294			assert.True(t, strings.Contains(output, testRepositoryV2.LatestManifest.Digest))
295			// basic text format doesn't include blob data
296			assert.False(t, strings.Contains(output, testRepositoryV2.LatestManifest.Blobs[0].Digest))
297		})
298	})
299	t.Run("with <none> latest tag", func(t *testing.T) {
300		withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
301			tm.registry.EXPECT().Get().Return(&testRegistry, nil)
302			tm.registry.EXPECT().ListRepositoriesV2(testRepositoryV2NoTags.RegistryName).Return([]do.RepositoryV2{testRepositoryV2NoTags}, nil)
303
304			var buf bytes.Buffer
305			config.Out = &buf
306			err := RunListRepositoriesV2(config)
307			assert.NoError(t, err)
308
309			output := buf.String()
310			// instead of trying to match the output, do some basic content checks
311			assert.True(t, strings.Contains(output, testRepositoryV2NoTags.Name))
312			assert.True(t, strings.Contains(output, "<none>")) // default value when latest manifest has no tags
313			// basic text format doesn't include blob data
314			assert.False(t, strings.Contains(output, testRepositoryV2NoTags.LatestManifest.Blobs[0].Digest))
315		})
316	})
317}
318
319func TestRepositoryListTags(t *testing.T) {
320	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
321		tm.registry.EXPECT().Get().Return(&testRegistry, nil)
322		tm.registry.EXPECT().ListRepositoryTags(
323			testRepositoryTag.RegistryName,
324			testRepositoryTag.Repository,
325		).Return([]do.RepositoryTag{testRepositoryTag}, nil)
326		config.Args = append(config.Args, testRepositoryTag.Repository)
327
328		err := RunListRepositoryTags(config)
329		assert.NoError(t, err)
330	})
331}
332
333func TestRepositoryDeleteTag(t *testing.T) {
334	tests := []struct {
335		name        string
336		args        []string
337		expect      func(m *mocks.MockRegistryService)
338		expectedErr string
339	}{
340		{
341			name: "no deletion arguments",
342			args: []string{
343				testRepositoryTag.Repository,
344			},
345			expectedErr: "(test) command is missing required arguments",
346		},
347		{
348			name: "one tag, successful",
349			args: []string{
350				testRepositoryTag.Repository,
351				testRepositoryTag.Tag,
352			},
353			expect: func(m *mocks.MockRegistryService) {
354				m.EXPECT().Get().Return(&testRegistry, nil)
355				m.EXPECT().DeleteTag(
356					testRepositoryTag.RegistryName,
357					testRepositoryTag.Repository,
358					testRepositoryTag.Tag,
359				).Return(nil)
360			},
361		},
362		{
363			name: "multiple tags, successful",
364			args: []string{
365				testRepositoryTag.Repository,
366				testRepositoryTag.Tag,
367				"extra-tag",
368			},
369			expect: func(m *mocks.MockRegistryService) {
370				m.EXPECT().Get().Return(&testRegistry, nil)
371				m.EXPECT().DeleteTag(
372					testRepositoryTag.RegistryName,
373					testRepositoryTag.Repository,
374					testRepositoryTag.Tag,
375				).Return(nil)
376				m.EXPECT().DeleteTag(
377					testRepositoryTag.RegistryName,
378					testRepositoryTag.Repository,
379					"extra-tag",
380				).Return(nil)
381			},
382		},
383		{
384			name: "multiple tags, partial failure",
385			args: []string{
386				testRepositoryTag.Repository,
387				"fail-tag",
388				testRepositoryTag.Tag,
389			},
390			expect: func(m *mocks.MockRegistryService) {
391				m.EXPECT().Get().Return(&testRegistry, nil)
392				m.EXPECT().DeleteTag(
393					testRepositoryTag.RegistryName,
394					testRepositoryTag.Repository,
395					"fail-tag",
396				).Return(errors.New("oops"))
397				m.EXPECT().DeleteTag(
398					testRepositoryTag.RegistryName,
399					testRepositoryTag.Repository,
400					testRepositoryTag.Tag,
401				).Return(nil)
402			},
403			expectedErr: "failed to delete all repository tags: \noops",
404		},
405	}
406
407	for _, test := range tests {
408		test := test
409		t.Run(test.name, func(t *testing.T) {
410			withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
411				if test.expect != nil {
412					test.expect(tm.registry)
413				}
414
415				config.Doit.Set(config.NS, doctl.ArgForce, true)
416				config.Args = append(config.Args, test.args...)
417
418				err := RunRepositoryDeleteTag(config)
419				if test.expectedErr == "" {
420					assert.NoError(t, err)
421				} else {
422					assert.Error(t, err)
423					assert.Equal(t, test.expectedErr, err.Error())
424				}
425			})
426		})
427	}
428}
429
430func TestRepositoryListManifests(t *testing.T) {
431	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
432		tm.registry.EXPECT().Get().Return(&testRegistry, nil)
433		tm.registry.EXPECT().ListRepositoryManifests(
434			testRepositoryManifest.RegistryName,
435			testRepositoryManifest.Repository,
436		).Return([]do.RepositoryManifest{testRepositoryManifest}, nil)
437
438		var buf bytes.Buffer
439		config.Out = &buf
440		config.Args = append(config.Args, testRepositoryManifest.Repository)
441
442		err := RunListRepositoryManifests(config)
443		assert.NoError(t, err)
444
445		output := buf.String()
446		// instead of trying to match the output, do some basic content checks
447		assert.True(t, strings.Contains(output, testRepositoryManifest.Digest))
448		assert.True(t, strings.Contains(output, fmt.Sprintf("%s", testRepositoryManifest.Tags)))
449		// basic text format doesn't include blob data
450		assert.False(t, strings.Contains(output, testRepositoryManifest.Blobs[0].Digest))
451	})
452}
453
454func TestRepositoryDeleteManifest(t *testing.T) {
455	extraDigest := "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270"
456	tests := []struct {
457		name        string
458		args        []string
459		expect      func(m *mocks.MockRegistryService)
460		expectedErr string
461	}{
462		{
463			name: "no deletion arguments",
464			args: []string{
465				testRepositoryTag.Repository,
466			},
467			expectedErr: "(test) command is missing required arguments",
468		},
469		{
470			name: "one digest, successful",
471			args: []string{
472				testRepositoryTag.Repository,
473				testRepositoryTag.ManifestDigest,
474			},
475			expect: func(m *mocks.MockRegistryService) {
476				m.EXPECT().Get().Return(&testRegistry, nil)
477				m.EXPECT().DeleteManifest(
478					testRepositoryTag.RegistryName,
479					testRepositoryTag.Repository,
480					testRepositoryTag.ManifestDigest,
481				).Return(nil)
482			},
483		},
484		{
485			name: "multiple digests, successful",
486			args: []string{
487				testRepositoryTag.Repository,
488				testRepositoryTag.ManifestDigest,
489				extraDigest,
490			},
491			expect: func(m *mocks.MockRegistryService) {
492				m.EXPECT().Get().Return(&testRegistry, nil)
493				m.EXPECT().DeleteManifest(
494					testRepositoryTag.RegistryName,
495					testRepositoryTag.Repository,
496					testRepositoryTag.ManifestDigest,
497				).Return(nil)
498				m.EXPECT().DeleteManifest(
499					testRepositoryTag.RegistryName,
500					testRepositoryTag.Repository,
501					extraDigest,
502				).Return(nil)
503			},
504		},
505		{
506			name: "multiple digests, partial failure",
507			args: []string{
508				testRepositoryTag.Repository,
509				"fail-digest",
510				testRepositoryTag.ManifestDigest,
511			},
512			expect: func(m *mocks.MockRegistryService) {
513				m.EXPECT().Get().Return(&testRegistry, nil)
514				m.EXPECT().DeleteManifest(
515					testRepositoryTag.RegistryName,
516					testRepositoryTag.Repository,
517					"fail-digest",
518				).Return(errors.New("oops"))
519				m.EXPECT().DeleteManifest(
520					testRepositoryTag.RegistryName,
521					testRepositoryTag.Repository,
522					testRepositoryTag.ManifestDigest,
523				).Return(nil)
524			},
525			expectedErr: "failed to delete all repository manifests: \noops",
526		},
527	}
528
529	for _, test := range tests {
530		test := test
531		t.Run(test.name, func(t *testing.T) {
532			withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
533				if test.expect != nil {
534					test.expect(tm.registry)
535				}
536
537				config.Doit.Set(config.NS, doctl.ArgForce, true)
538				config.Args = append(config.Args, test.args...)
539
540				err := RunRepositoryDeleteManifest(config)
541				if test.expectedErr == "" {
542					assert.NoError(t, err)
543				} else {
544					assert.Error(t, err)
545					assert.Equal(t, test.expectedErr, err.Error())
546				}
547			})
548		})
549	}
550}
551
552func TestRegistryKubernetesManifest(t *testing.T) {
553	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
554		// test cases
555		tcs := []struct {
556			argName, argNamespace           string
557			expectedName, expectedNamespace string
558			expectedAnnotations             map[string]string
559		}{
560			{
561				argName:           "",
562				argNamespace:      "default",
563				expectedName:      "registry-" + testRegistry.Name,
564				expectedNamespace: "default",
565			},
566			{
567				argName:           "my-registry",
568				argNamespace:      "secret-namespace",
569				expectedName:      "my-registry",
570				expectedNamespace: "secret-namespace",
571			},
572			{
573				argName:           "my-registry",
574				argNamespace:      "kube-system",
575				expectedName:      "my-registry",
576				expectedNamespace: "kube-system",
577				expectedAnnotations: map[string]string{
578					DOSecretOperatorAnnotation: "my-registry",
579				},
580			},
581		}
582
583		// mocks shared across both test cases
584		// Get should be called only when a name isn't supplied, to look up the registry's name
585		tm.registry.EXPECT().Get().Return(&testRegistry, nil).Times(1)
586		// DockerCredentials should be called both times to retrieve the credentials
587		tm.registry.EXPECT().DockerCredentials(&godo.RegistryDockerCredentialsRequest{
588			ReadWrite: false,
589		}).Return(testDockerCredentials, nil).Times(len(tcs))
590
591		// tests
592		for _, tc := range tcs {
593			config.Doit.Set(config.NS, doctl.ArgObjectName, tc.argName)
594			config.Doit.Set(config.NS, doctl.ArgObjectNamespace, tc.argNamespace)
595
596			var outputBuffer bytes.Buffer
597			config.Out = &outputBuffer
598			err := RunKubernetesManifest(config)
599			assert.NoError(t, err)
600
601			// check the object
602			obj, _, err := k8sscheme.Codecs.UniversalDeserializer().Decode(outputBuffer.Bytes(), nil, nil)
603			assert.NoError(t, err)
604			secret := obj.(*k8sapiv1.Secret)
605
606			assert.Equal(t, "Secret", secret.TypeMeta.Kind)
607			assert.Equal(t, "v1", secret.TypeMeta.APIVersion)
608			assert.Equal(t, k8sapiv1.SecretTypeDockerConfigJson, secret.Type)
609			assert.Equal(t, tc.expectedName, secret.ObjectMeta.Name)
610			assert.Equal(t, tc.expectedNamespace, secret.ObjectMeta.Namespace)
611			assert.Contains(t, secret.Data, ".dockerconfigjson")
612			assert.Equal(t, secret.Data[".dockerconfigjson"], testDockerCredentials.DockerConfigJSON)
613			assert.Equal(t, tc.expectedAnnotations, secret.Annotations)
614		}
615	})
616}
617
618func TestRegistryLogin(t *testing.T) {
619	tests := []struct {
620		name          string
621		expirySeconds int
622		expect        func(m *mocks.MockRegistryService)
623	}{
624		{
625			name:          "no-expiry",
626			expirySeconds: 0,
627			expect: func(m *mocks.MockRegistryService) {
628				m.EXPECT().Endpoint().Return(do.RegistryHostname)
629				m.EXPECT().DockerCredentials(&godo.RegistryDockerCredentialsRequest{
630					ReadWrite: true,
631				}).Return(testDockerCredentials, nil)
632			},
633		},
634		{
635			name:          "with-expiry",
636			expirySeconds: 3600,
637			expect: func(m *mocks.MockRegistryService) {
638				m.EXPECT().Endpoint().Return(do.RegistryHostname)
639				m.EXPECT().DockerCredentials(&godo.RegistryDockerCredentialsRequest{
640					ReadWrite:     true,
641					ExpirySeconds: godo.Int(3600),
642				}).Return(testDockerCredentials, nil)
643			},
644		},
645	}
646
647	for _, test := range tests {
648		t.Run(test.name, func(t *testing.T) {
649			withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
650				if test.expect != nil {
651					test.expect(tm.registry)
652				}
653
654				config.Doit.Set(config.NS, doctl.ArgRegistryExpirySeconds, test.expirySeconds)
655
656				config.Out = os.Stderr
657				err := RunRegistryLogin(config)
658				assert.NoError(t, err)
659			})
660		})
661	}
662}
663
664func TestRegistryLogout(t *testing.T) {
665	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
666		config.Doit.Set(config.NS, doctl.ArgRegistryAuthorizationServerEndpoint, "http://example.com")
667		tm.registry.EXPECT().Endpoint().Return(do.RegistryHostname)
668		tm.registry.EXPECT().RevokeOAuthToken(gomock.Any(), "http://example.com").Times(1).Return(nil)
669
670		config.Out = os.Stderr
671		err := RunRegistryLogout(config)
672		assert.NoError(t, err)
673	})
674}
675
676func TestGarbageCollectionStart(t *testing.T) {
677	defaultStartGCRequest := &godo.StartGarbageCollectionRequest{
678		Type: godo.GCTypeUnreferencedBlobsOnly,
679	}
680	tests := []struct {
681		name      string
682		extraArgs []string
683
684		expect      func(m *mocks.MockRegistryService, config *CmdConfig)
685		expectError error
686	}{
687		{
688			name: "without registry name arg",
689		},
690		{
691			name: "with registry name arg",
692			extraArgs: []string{
693				testRegistryName,
694			},
695			expect: func(m *mocks.MockRegistryService, config *CmdConfig) {
696				config.Doit.Set(config.NS, doctl.ArgForce, true)
697				m.EXPECT().StartGarbageCollection(testRegistry.Name, defaultStartGCRequest).Return(testGarbageCollection, nil)
698			},
699		},
700		{
701			name: "include untagged manifests",
702			extraArgs: []string{
703				testRegistryName,
704			},
705			expect: func(m *mocks.MockRegistryService, config *CmdConfig) {
706				config.Doit.Set(config.NS, doctl.ArgForce, true)
707				config.Doit.Set(config.NS, doctl.ArgGCIncludeUntaggedManifests, true)
708				config.Doit.Set(config.NS, doctl.ArgGCExcludeUnreferencedBlobs, false)
709				m.EXPECT().StartGarbageCollection(testRegistry.Name, &godo.StartGarbageCollectionRequest{
710					Type: godo.GCTypeUntaggedManifestsAndUnreferencedBlobs,
711				}).Return(testGarbageCollection, nil)
712			},
713		},
714		{
715			name: "include untagged manifests, exclude unreferenced blobs",
716			extraArgs: []string{
717				testRegistryName,
718			},
719			expect: func(m *mocks.MockRegistryService, config *CmdConfig) {
720				config.Doit.Set(config.NS, doctl.ArgForce, true)
721				config.Doit.Set(config.NS, doctl.ArgGCIncludeUntaggedManifests, true)
722				config.Doit.Set(config.NS, doctl.ArgGCExcludeUnreferencedBlobs, true)
723				m.EXPECT().StartGarbageCollection(testRegistry.Name, &godo.StartGarbageCollectionRequest{
724					Type: godo.GCTypeUntaggedManifestsOnly,
725				}).Return(testGarbageCollection, nil)
726			},
727		},
728		{
729			name: "fail with invalid combination of gc targets",
730			extraArgs: []string{
731				invalidRegistryName,
732			},
733			expect: func(m *mocks.MockRegistryService, config *CmdConfig) {
734				config.Doit.Set(config.NS, doctl.ArgForce, true)
735				config.Doit.Set(config.NS, doctl.ArgGCIncludeUntaggedManifests, false)
736				config.Doit.Set(config.NS, doctl.ArgGCExcludeUnreferencedBlobs, true)
737			},
738			expectError: fmt.Errorf("incompatible combination of include-untagged-manifests and exclude-unreferenced-blobs flags"),
739		},
740		{
741			name: "fail with invalid registry name arg",
742			extraArgs: []string{
743				invalidRegistryName,
744			},
745			expect: func(m *mocks.MockRegistryService, config *CmdConfig) {
746				config.Doit.Set(config.NS, doctl.ArgForce, true)
747				m.EXPECT().StartGarbageCollection(invalidRegistryName, defaultStartGCRequest).Return(nil, fmt.Errorf("meow"))
748			},
749			expectError: fmt.Errorf("meow"),
750		},
751		{
752			name: "fail with too many args",
753			extraArgs: []string{
754				invalidRegistryName,
755				testGCUUID,
756			},
757			expect:      func(m *mocks.MockRegistryService, config *CmdConfig) {},
758			expectError: fmt.Errorf("(test) command contains unsupported arguments"),
759		},
760		{
761			name: "prompt to confirm without --force argument",
762			extraArgs: []string{
763				testRegistryName,
764			},
765			expect:      func(m *mocks.MockRegistryService, config *CmdConfig) {},
766			expectError: errOperationAborted,
767		},
768	}
769	for _, test := range tests {
770		t.Run(test.name, func(t *testing.T) {
771			withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
772				if test.expect != nil {
773					test.expect(tm.registry, config)
774				} else {
775					config.Doit.Set(config.NS, doctl.ArgForce, true)
776					tm.registry.EXPECT().Get().Return(&testRegistry, nil)
777					tm.registry.EXPECT().StartGarbageCollection(testRegistry.Name, defaultStartGCRequest).Return(testGarbageCollection, nil)
778				}
779
780				if test.extraArgs != nil {
781					config.Args = append(config.Args, test.extraArgs...)
782				}
783
784				err := RunStartGarbageCollection(config)
785
786				if test.expectError != nil {
787					assert.Error(t, err)
788					assert.Equal(t, test.expectError.Error(), err.Error())
789				} else {
790					assert.NoError(t, err)
791				}
792			})
793		})
794	}
795}
796
797func TestGarbageCollectionGetActive(t *testing.T) {
798	tests := []struct {
799		name        string
800		extraArgs   []string
801		expect      func(m *mocks.MockRegistryService)
802		expectError error
803	}{
804		{
805			name: "without registry name arg",
806		},
807		{
808			name: "with registry name arg",
809			extraArgs: []string{
810				testRegistryName,
811			},
812			expect: func(m *mocks.MockRegistryService) {
813				m.EXPECT().GetGarbageCollection(testRegistry.Name).Return(testGarbageCollection, nil)
814			},
815		},
816		{
817			name: "fail with invalid registry name arg",
818			extraArgs: []string{
819				invalidRegistryName,
820			},
821			expect: func(m *mocks.MockRegistryService) {
822				m.EXPECT().GetGarbageCollection(invalidRegistryName).Return(nil, fmt.Errorf("meow"))
823			},
824			expectError: fmt.Errorf("meow"),
825		},
826		{
827			name: "fail with too many args",
828			extraArgs: []string{
829				invalidRegistryName,
830				testRegistryName,
831			},
832			expect:      func(m *mocks.MockRegistryService) {},
833			expectError: fmt.Errorf("(test) command contains unsupported arguments"),
834		},
835	}
836	for _, test := range tests {
837		t.Run(test.name, func(t *testing.T) {
838			withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
839				if test.expect != nil {
840					test.expect(tm.registry)
841				} else {
842					tm.registry.EXPECT().Get().Return(&testRegistry, nil)
843					tm.registry.EXPECT().GetGarbageCollection(testRegistry.Name).Return(testGarbageCollection, nil)
844				}
845
846				if test.extraArgs != nil {
847					config.Args = append(config.Args, test.extraArgs...)
848				}
849
850				err := RunGetGarbageCollection(config)
851
852				if test.expectError != nil {
853					assert.Error(t, err)
854					assert.Equal(t, test.expectError.Error(), err.Error())
855				} else {
856					assert.NoError(t, err)
857				}
858			})
859		})
860	}
861}
862
863func TestGarbageCollectionList(t *testing.T) {
864	tests := []struct {
865		name      string
866		extraArgs []string
867
868		expect      func(m *mocks.MockRegistryService)
869		expectError error
870	}{
871		{
872			name: "without registry name arg",
873		},
874		{
875			name: "with registry name arg",
876			extraArgs: []string{
877				testRegistryName,
878			},
879			expect: func(m *mocks.MockRegistryService) {
880				m.EXPECT().ListGarbageCollections(testRegistry.Name).Return([]do.GarbageCollection{*testGarbageCollection}, nil)
881			},
882		},
883		{
884			name: "fail with invalid registry name arg",
885			extraArgs: []string{
886				invalidRegistryName,
887			},
888			expect: func(m *mocks.MockRegistryService) {
889				m.EXPECT().ListGarbageCollections(invalidRegistryName).Return(nil, fmt.Errorf("meow"))
890			},
891			expectError: fmt.Errorf("meow"),
892		},
893		{
894			name: "fail with too many args",
895			extraArgs: []string{
896				invalidRegistryName,
897				testGCUUID,
898			},
899			expect:      func(m *mocks.MockRegistryService) {},
900			expectError: fmt.Errorf("(test) command contains unsupported arguments"),
901		},
902	}
903	for _, test := range tests {
904		t.Run(test.name, func(t *testing.T) {
905			withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
906				if test.expect != nil {
907					test.expect(tm.registry)
908				} else {
909					tm.registry.EXPECT().Get().Return(&testRegistry, nil)
910					tm.registry.EXPECT().ListGarbageCollections(testRegistry.Name).Return([]do.GarbageCollection{*testGarbageCollection}, nil)
911				}
912
913				if test.extraArgs != nil {
914					config.Args = append(config.Args, test.extraArgs...)
915				}
916
917				err := RunListGarbageCollections(config)
918
919				if test.expectError != nil {
920					assert.Error(t, err)
921					assert.Equal(t, test.expectError.Error(), err.Error())
922				} else {
923					assert.NoError(t, err)
924				}
925			})
926		})
927	}
928}
929
930func TestGarbageCollectionUpdate(t *testing.T) {
931	tests := []struct {
932		name        string
933		extraArgs   []string
934		expect      func(m *mocks.MockRegistryService)
935		expectError error
936	}{
937		{
938			name: "with gc uuid arg",
939			extraArgs: []string{
940				testGCUUID,
941			},
942			expect: func(m *mocks.MockRegistryService) {
943				m.EXPECT().Get().Return(&testRegistry, nil)
944				m.EXPECT().CancelGarbageCollection(testRegistry.Name, testGCUUID).Return(testGarbageCollection, nil)
945			},
946		},
947		{
948			name: "with registry name and gc uuid arg",
949			extraArgs: []string{
950				testRegistryName,
951				testGCUUID,
952			},
953			expect: func(m *mocks.MockRegistryService) {
954				m.EXPECT().CancelGarbageCollection(testRegistryName, testGCUUID).Return(testGarbageCollection, nil)
955			},
956		},
957		{
958			name: "fail with invalid registry name arg",
959			extraArgs: []string{
960				invalidRegistryName,
961				testGCUUID,
962			},
963			expect: func(m *mocks.MockRegistryService) {
964				m.EXPECT().CancelGarbageCollection(invalidRegistryName, testGCUUID).Return(nil, fmt.Errorf("meow"))
965			},
966			expectError: fmt.Errorf("meow"),
967		},
968		{
969			name: "fail with invalid gc uuid arg and valid registry name arg",
970			extraArgs: []string{
971				testRegistryName,
972				invalidGCUUID,
973			},
974			expect: func(m *mocks.MockRegistryService) {
975				m.EXPECT().CancelGarbageCollection(testRegistryName, invalidGCUUID).Return(nil, fmt.Errorf("meow"))
976			},
977			expectError: fmt.Errorf("meow"),
978		},
979		{
980			name: "fail with invalid gc uuid arg",
981			extraArgs: []string{
982				invalidGCUUID,
983			},
984			expect: func(m *mocks.MockRegistryService) {
985				m.EXPECT().Get().Return(&testRegistry, nil)
986				m.EXPECT().CancelGarbageCollection(testRegistryName, invalidGCUUID).Return(nil, fmt.Errorf("meow"))
987			},
988			expectError: fmt.Errorf("meow"),
989		},
990		{
991			name: "fail with too many args",
992			extraArgs: []string{
993				invalidRegistryName,
994				testGCUUID,
995				testGCUUID,
996			},
997			expect:      func(m *mocks.MockRegistryService) {},
998			expectError: fmt.Errorf("(test) command contains unsupported arguments"),
999		},
1000		{
1001			name:        "fail with no args",
1002			extraArgs:   []string{},
1003			expect:      func(m *mocks.MockRegistryService) {},
1004			expectError: fmt.Errorf("(test) command is missing required arguments"),
1005		},
1006	}
1007	for _, test := range tests {
1008		t.Run(test.name, func(t *testing.T) {
1009			withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
1010				test.expect(tm.registry)
1011
1012				if test.extraArgs != nil {
1013					config.Args = append(config.Args, test.extraArgs...)
1014				}
1015
1016				err := RunCancelGarbageCollection(config)
1017
1018				if test.expectError != nil {
1019					assert.Error(t, err)
1020					assert.Equal(t, test.expectError.Error(), err.Error())
1021				} else {
1022					assert.NoError(t, err)
1023				}
1024			})
1025		})
1026	}
1027}
1028