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