|
1 | 1 | // |
2 | | -// Copyright 2024-2025 The Chainloop Authors. |
| 2 | +// Copyright 2024-2026 The Chainloop Authors. |
3 | 3 | // |
4 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); |
5 | 5 | // you may not use this file except in compliance with the License. |
@@ -264,6 +264,95 @@ func (s *casBackendTestSuite) TestNewCASBackendUseCase() { |
264 | 264 | } |
265 | 265 | } |
266 | 266 |
|
| 267 | +// TestUpdateRotatesCredentialsInPlace verifies that Update() passes the existing SecretName |
| 268 | +// as a WithExistingSecret option so the credential store upserts in-place instead of |
| 269 | +// creating a new entry. |
| 270 | +func (s *casBackendTestSuite) TestUpdateRotatesCredentialsInPlace() { |
| 271 | + ctx := context.Background() |
| 272 | + existingSecretName := "org/existing-secret-path" |
| 273 | + backendID := uuid.New() |
| 274 | + newCreds := &credentials.OCIKeypair{Repo: "r", Username: "u", Password: "p"} |
| 275 | + |
| 276 | + tests := []struct { |
| 277 | + name string |
| 278 | + existingSecret string |
| 279 | + wantWithExistingSecret bool // whether the SaveOption should carry the existing secret name |
| 280 | + returnedSecretName string // what SaveCredentials mock returns |
| 281 | + }{ |
| 282 | + { |
| 283 | + name: "existing secret name is forwarded as WithExistingSecret", |
| 284 | + existingSecret: existingSecretName, |
| 285 | + wantWithExistingSecret: true, |
| 286 | + returnedSecretName: existingSecretName, |
| 287 | + }, |
| 288 | + { |
| 289 | + name: "empty secret name generates a new path", |
| 290 | + existingSecret: "", |
| 291 | + wantWithExistingSecret: false, |
| 292 | + returnedSecretName: "new-secret-path", |
| 293 | + }, |
| 294 | + } |
| 295 | + |
| 296 | + for _, tc := range tests { |
| 297 | + s.Run(tc.name, func() { |
| 298 | + s.resetMock() |
| 299 | + |
| 300 | + before := &biz.CASBackend{ |
| 301 | + ID: backendID, |
| 302 | + SecretName: tc.existingSecret, |
| 303 | + Provider: backendType, |
| 304 | + } |
| 305 | + |
| 306 | + // Step 1: FindByIDInOrg returns the existing backend (consumed once). |
| 307 | + s.repo.On("FindByIDInOrg", ctx, s.validUUID, backendID).Return(before, nil).Once() |
| 308 | + |
| 309 | + // Step 2: SaveCredentials — capture SaveOption(s) to verify the secret name. |
| 310 | + var capturedSecretName string |
| 311 | + var optWasPassed bool |
| 312 | + saveMatcher := mock.MatchedBy(func(_ interface{}) bool { return true }) |
| 313 | + s.credsRW.On("SaveCredentials", ctx, s.validUUID.String(), saveMatcher, mock.Anything). |
| 314 | + Run(func(args mock.Arguments) { |
| 315 | + if len(args) > 3 { |
| 316 | + optWasPassed = true |
| 317 | + if opt, ok := args.Get(3).(credentials.SaveOption); ok { |
| 318 | + o := credentials.ApplySaveOptions(opt) |
| 319 | + capturedSecretName = o.SecretName |
| 320 | + } |
| 321 | + } |
| 322 | + }).Return(tc.returnedSecretName, nil).Maybe() |
| 323 | + |
| 324 | + // Fallback for no-opts case (empty existing secret → no WithExistingSecret). |
| 325 | + s.credsRW.On("SaveCredentials", ctx, s.validUUID.String(), saveMatcher). |
| 326 | + Return(tc.returnedSecretName, nil).Maybe() |
| 327 | + |
| 328 | + // Step 3: repo.Update persists the change. |
| 329 | + updatedBackend := &biz.CASBackend{ID: backendID, SecretName: tc.returnedSecretName, Provider: backendType} |
| 330 | + s.repo.On("Update", ctx, mock.Anything).Return(updatedBackend, nil) |
| 331 | + |
| 332 | + // Steps 4–7: PerformValidation is called internally with new creds. |
| 333 | + s.repo.On("FindByID", mock.Anything, backendID).Return(updatedBackend, nil) |
| 334 | + s.credsRW.On("ReadCredentials", mock.Anything, mock.Anything, mock.Anything).Return(nil) |
| 335 | + s.backendProvider.On("ValidateAndExtractCredentials", mock.Anything, mock.Anything).Return(nil, nil) |
| 336 | + s.repo.On("UpdateValidationStatus", mock.Anything, backendID, biz.CASBackendValidationOK, mock.Anything).Return(nil) |
| 337 | + |
| 338 | + // Step 8: FindByIDInOrg reload after validation. |
| 339 | + s.repo.On("FindByIDInOrg", ctx, s.validUUID, backendID).Return(updatedBackend, nil) |
| 340 | + |
| 341 | + got, err := s.useCase.Update(ctx, s.validUUID.String(), backendID.String(), nil, newCreds, nil, nil, nil) |
| 342 | + s.Require().NoError(err) |
| 343 | + s.Equal(tc.returnedSecretName, got.SecretName) |
| 344 | + |
| 345 | + if tc.wantWithExistingSecret { |
| 346 | + s.True(optWasPassed, "expected SaveOption to be passed to SaveCredentials") |
| 347 | + s.Equal(existingSecretName, capturedSecretName, |
| 348 | + "expected WithExistingSecret(%q) to be forwarded to SaveCredentials", existingSecretName) |
| 349 | + } else { |
| 350 | + s.False(optWasPassed, "expected no SaveOption to be passed to SaveCredentials") |
| 351 | + } |
| 352 | + }) |
| 353 | + } |
| 354 | +} |
| 355 | + |
267 | 356 | // Run all the tests |
268 | 357 | func TestCASBackend(t *testing.T) { |
269 | 358 | suite.Run(t, new(casBackendTestSuite)) |
|
0 commit comments