github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/cd-service/pkg/service/batch_test.go (about)

     1  /*This file is part of kuberpult.
     2  
     3  Kuberpult is free software: you can redistribute it and/or modify
     4  it under the terms of the Expat(MIT) License as published by
     5  the Free Software Foundation.
     6  
     7  Kuberpult is distributed in the hope that it will be useful,
     8  but WITHOUT ANY WARRANTY; without even the implied warranty of
     9  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    10  MIT License for more details.
    11  
    12  You should have received a copy of the MIT License
    13  along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>.
    14  
    15  Copyright 2023 freiheit.com*/
    16  
    17  package service
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os/exec"
    23  	"path"
    24  	"path/filepath"
    25  	"testing"
    26  
    27  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/repository/testutil"
    28  
    29  	"github.com/google/go-cmp/cmp"
    30  	"github.com/google/go-cmp/cmp/cmpopts"
    31  	"google.golang.org/grpc/status"
    32  	"google.golang.org/protobuf/testing/protocmp"
    33  
    34  	api "github.com/freiheit-com/kuberpult/pkg/api/v1"
    35  	"github.com/freiheit-com/kuberpult/pkg/auth"
    36  	"github.com/freiheit-com/kuberpult/pkg/ptr"
    37  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/config"
    38  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/repository"
    39  )
    40  
    41  // Used to compare two error message strings, needed because errors.Is(fmt.Errorf(text),fmt.Errorf(text)) == false
    42  type errMatcher struct {
    43  	msg string
    44  }
    45  
    46  func (e errMatcher) Error() string {
    47  	return e.msg
    48  }
    49  
    50  func (e errMatcher) Is(err error) bool {
    51  	return e.Error() == err.Error()
    52  }
    53  
    54  func getBatchActions() []*api.BatchAction {
    55  	opDeploy := &api.BatchAction_Deploy{
    56  		Deploy: &api.DeployRequest{
    57  			Environment:  "production",
    58  			Application:  "test",
    59  			Version:      1,
    60  			LockBehavior: api.LockBehavior_FAIL,
    61  		},
    62  	}
    63  	opCreateEnvLock := &api.BatchAction_CreateEnvironmentLock{
    64  		CreateEnvironmentLock: &api.CreateEnvironmentLockRequest{
    65  			Environment: "production",
    66  			LockId:      "envlock",
    67  			Message:     "please",
    68  		},
    69  	}
    70  	opCreateAppLock := &api.BatchAction_CreateEnvironmentApplicationLock{
    71  		CreateEnvironmentApplicationLock: &api.CreateEnvironmentApplicationLockRequest{
    72  			Environment: "production",
    73  			Application: "test",
    74  			LockId:      "applock",
    75  			Message:     "please",
    76  		},
    77  	}
    78  	opDeleteEnvLock := &api.BatchAction_DeleteEnvironmentLock{ // this deletes the existing lock in the transformers
    79  		DeleteEnvironmentLock: &api.DeleteEnvironmentLockRequest{
    80  			Environment: "production",
    81  			LockId:      "1234",
    82  		},
    83  	}
    84  	opDeleteAppLock := &api.BatchAction_DeleteEnvironmentApplicationLock{ // this deletes the existing lock in the transformers
    85  		DeleteEnvironmentApplicationLock: &api.DeleteEnvironmentApplicationLockRequest{
    86  			Environment: "production",
    87  			Application: "test",
    88  			LockId:      "5678",
    89  		},
    90  	}
    91  	ops := []*api.BatchAction{ // it works through the batch in order
    92  		{Action: opDeleteEnvLock},
    93  		{Action: opDeleteAppLock},
    94  		{Action: opDeploy},
    95  		{Action: opCreateEnvLock},
    96  		{Action: opCreateAppLock},
    97  	}
    98  	return ops
    99  }
   100  
   101  func getNBatchActions(N int) []*api.BatchAction {
   102  	var ops []*api.BatchAction
   103  	for i := 1; i <= N; i++ {
   104  		deploy := api.DeployRequest{
   105  			Environment:  "production",
   106  			Application:  "test",
   107  			Version:      1,
   108  			LockBehavior: api.LockBehavior_FAIL,
   109  		}
   110  		if i%2 == 0 {
   111  			deploy.Version = 2
   112  		}
   113  		ops = append(ops, &api.BatchAction{
   114  			Action: &api.BatchAction_Deploy{
   115  				Deploy: &deploy,
   116  			},
   117  		})
   118  	}
   119  	return ops
   120  }
   121  
   122  func TestBatchServiceWorks(t *testing.T) {
   123  	const prod = "production"
   124  	tcs := []struct {
   125  		Name          string
   126  		Batch         []*api.BatchAction
   127  		Setup         []repository.Transformer
   128  		context       context.Context
   129  		svc           *BatchServer
   130  		expectedError error
   131  	}{
   132  		{
   133  			Name: "5 sample actions",
   134  			Setup: []repository.Transformer{
   135  				&repository.CreateEnvironment{
   136  					Environment: prod,
   137  					Config:      config.EnvironmentConfig{Upstream: &config.EnvironmentConfigUpstream{Latest: true}},
   138  				},
   139  				&repository.CreateApplicationVersion{
   140  					Application: "test",
   141  					Manifests: map[string]string{
   142  						prod: "manifest",
   143  					},
   144  				},
   145  				&repository.CreateEnvironmentLock{ // will be deleted by the batch actions
   146  					Environment: prod,
   147  					LockId:      "1234",
   148  					Message:     "EnvLock",
   149  				},
   150  				&repository.CreateEnvironmentApplicationLock{ // will be deleted by the batch actions
   151  					Environment: prod,
   152  					Application: "test",
   153  					LockId:      "5678",
   154  					Message:     "AppLock",
   155  				},
   156  			},
   157  			Batch:   getBatchActions(),
   158  			context: testutil.MakeTestContext(),
   159  			svc:     &BatchServer{},
   160  		},
   161  		{
   162  			Name: "testing Dex setup with permissions",
   163  			Setup: []repository.Transformer{
   164  				&repository.CreateEnvironment{
   165  					Environment: "production",
   166  					Config:      config.EnvironmentConfig{Upstream: &config.EnvironmentConfigUpstream{Latest: true}},
   167  				},
   168  				&repository.CreateApplicationVersion{
   169  					Application: "test",
   170  					Manifests: map[string]string{
   171  						"production": "manifest",
   172  					},
   173  				},
   174  				&repository.CreateEnvironmentLock{
   175  					Environment: "production",
   176  					LockId:      "1234",
   177  					Message:     "EnvLock",
   178  				},
   179  				&repository.CreateEnvironmentApplicationLock{
   180  					Environment: "production",
   181  					Application: "test",
   182  					LockId:      "5678",
   183  					Message:     "no message",
   184  				},
   185  			},
   186  			Batch:   getBatchActions(),
   187  			context: testutil.MakeTestContextDexEnabled(),
   188  			svc: &BatchServer{
   189  				RBACConfig: auth.RBACConfig{
   190  					DexEnabled: true,
   191  					Policy: map[string]*auth.Permission{
   192  						"developer,DeployRelease,production:production,*,allow": {Role: "Developer"},
   193  						"developer,CreateLock,production:production,*,allow":    {Role: "Developer"},
   194  						"developer,DeleteLock,production:production,*,allow":    {Role: "Developer"},
   195  					},
   196  				},
   197  			},
   198  		},
   199  	}
   200  	for _, tc := range tcs {
   201  		tc := tc
   202  		t.Run(tc.Name, func(t *testing.T) {
   203  			repo, err := setupRepositoryTest(t)
   204  			if err != nil {
   205  				t.Fatal(err)
   206  			}
   207  			for _, tr := range tc.Setup {
   208  				err := repo.Apply(tc.context, tr)
   209  				if diff := cmp.Diff(tc.expectedError, err, cmpopts.EquateErrors()); diff != "" {
   210  					t.Fatalf("error mismatch (-want, +got):\n%s", diff)
   211  				}
   212  			}
   213  
   214  			tc.svc.Repository = repo
   215  			resp, err := tc.svc.ProcessBatch(
   216  				tc.context,
   217  				&api.BatchRequest{
   218  					Actions: tc.Batch,
   219  				},
   220  			)
   221  			if diff := cmp.Diff(tc.expectedError, err, cmpopts.EquateErrors()); diff != "" {
   222  				t.Fatalf("error mismatch (-want, +got):\n%s", diff)
   223  			}
   224  			if tc.expectedError != nil {
   225  				return
   226  			}
   227  
   228  			if len(resp.Results) != len(tc.Batch) {
   229  				t.Errorf("got wrong number of batch results, expected %d but got %d", len(tc.Batch), len(resp.Results))
   230  			}
   231  			// check deployment version
   232  			{
   233  				version, err := tc.svc.Repository.State().GetEnvironmentApplicationVersion("production", "test")
   234  				if err != nil {
   235  					t.Fatal(err)
   236  				}
   237  				if version == nil {
   238  					t.Errorf("unexpected version: expected 1, actual: %d", version)
   239  				}
   240  				if *version != 1 {
   241  					t.Errorf("unexpected version: expected 1, actual: %d", *version)
   242  				}
   243  			}
   244  			// check that the envlock was created/deleted
   245  			{
   246  				envLocks, err := tc.svc.Repository.State().GetEnvironmentLocks("production")
   247  				if err != nil {
   248  					t.Fatal(err)
   249  				}
   250  				lock, exists := envLocks["envlock"]
   251  				if !exists {
   252  					t.Error("lock was not created")
   253  				}
   254  				if lock.Message != "please" {
   255  					t.Errorf("unexpected lock message: expected \"please\", actual: %q", lock.Message)
   256  				}
   257  				_, exists = envLocks["1234"]
   258  				if exists {
   259  					t.Error("lock was not deleted")
   260  				}
   261  			}
   262  			// check that the applock was created/deleted
   263  			{
   264  				appLocks, err := tc.svc.Repository.State().GetEnvironmentApplicationLocks("production", "test")
   265  				if err != nil {
   266  					t.Fatal(err)
   267  				}
   268  				lock, exists := appLocks["applock"]
   269  				if !exists {
   270  					t.Error("lock was not created")
   271  				}
   272  				if lock.Message != "please" {
   273  					t.Errorf("unexpected lock message: expected \"please\", actual: %q", lock.Message)
   274  				}
   275  				_, exists = appLocks["5678"]
   276  				if exists {
   277  					t.Error("lock was not deleted")
   278  				}
   279  			}
   280  
   281  		})
   282  	}
   283  }
   284  
   285  func TestBatchServiceFails(t *testing.T) {
   286  	tcs := []struct {
   287  		Name               string
   288  		Batch              []*api.BatchAction
   289  		Setup              []repository.Transformer
   290  		context            context.Context
   291  		svc                *BatchServer
   292  		expectedError      error
   293  		expectedSetupError error
   294  	}{
   295  		{
   296  			Name: "testing Dex setup without permissions",
   297  			Setup: []repository.Transformer{
   298  				&repository.CreateEnvironment{
   299  					Environment: "production",
   300  					Config:      config.EnvironmentConfig{Upstream: &config.EnvironmentConfigUpstream{Latest: true}},
   301  				},
   302  				&repository.CreateApplicationVersion{
   303  					Application: "test",
   304  					Manifests: map[string]string{
   305  						"production": "manifest",
   306  					},
   307  				},
   308  				&repository.CreateEnvironmentLock{ // will be deleted by the batch actions
   309  					Environment:    "production",
   310  					LockId:         "1234",
   311  					Message:        "EnvLock",
   312  					Authentication: repository.Authentication{RBACConfig: auth.RBACConfig{DexEnabled: true}},
   313  				},
   314  			},
   315  			Batch:   []*api.BatchAction{},
   316  			context: testutil.MakeTestContextDexEnabled(),
   317  			svc:     &BatchServer{},
   318  			// expectedSetupError: errMatcher{"error at index 0 of transformer batch: rpc error: code = PermissionDenied desc = PermissionDenied: The user 'test tester' with role 'developer' is not allowed to perform the action 'CreateLock' on environment 'production'"},
   319  			expectedSetupError: &repository.TransformerBatchApplyError{
   320  				Index: 0,
   321  				TransformerError: auth.PermissionError{
   322  					User:        "test tester",
   323  					Role:        "developer",
   324  					Action:      "CreateLock",
   325  					Environment: "production",
   326  				},
   327  			},
   328  		},
   329  	}
   330  	for _, tc := range tcs {
   331  		tc := tc
   332  		t.Run(tc.Name, func(t *testing.T) {
   333  			repo, err := setupRepositoryTest(t)
   334  			if err != nil {
   335  				t.Fatal(err)
   336  			}
   337  			errSetupObserved := false
   338  			for _, tr := range tc.Setup {
   339  				err := repo.Apply(tc.context, tr)
   340  				if err != nil {
   341  					if diff := cmp.Diff(tc.expectedSetupError, err, cmpopts.EquateErrors()); diff != "" {
   342  						t.Fatalf("error during setup mismatch (-want, +got):\n%s", diff)
   343  					} else {
   344  						errSetupObserved = true
   345  					}
   346  				}
   347  			}
   348  			if tc.expectedSetupError != nil && !errSetupObserved {
   349  				// ensure we fail on unobserved error
   350  				t.Errorf("did not oberve error during setup: %s", tc.expectedSetupError.Error())
   351  			}
   352  
   353  			tc.svc.Repository = repo
   354  			resp, err := tc.svc.ProcessBatch(
   355  				tc.context,
   356  				&api.BatchRequest{
   357  					Actions: tc.Batch,
   358  				},
   359  			)
   360  			if diff := cmp.Diff(tc.expectedError, err, cmpopts.EquateErrors()); diff != "" {
   361  				t.Fatalf("error mismatch (-want, +got):\n%s", diff)
   362  			}
   363  
   364  			if len(resp.Results) != len(tc.Batch) {
   365  				t.Errorf("got wrong number of batch results, expected %d but got %d", len(tc.Batch), len(resp.Results))
   366  			}
   367  		})
   368  	}
   369  }
   370  
   371  func TestBatchServiceErrors(t *testing.T) {
   372  	tcs := []struct {
   373  		Name             string
   374  		Batch            []*api.BatchAction
   375  		Setup            []repository.Transformer
   376  		ExpectedResponse *api.BatchResponse
   377  		ExpectedError    error
   378  	}{
   379  		{
   380  			// tests that in ProcessBatch, transformer errors are returned without wrapping them in a
   381  			// not so helpful "internal error"
   382  			Name:  "forwards transformers error to caller: cannot open manifest",
   383  			Setup: []repository.Transformer{},
   384  			Batch: []*api.BatchAction{
   385  				{
   386  					Action: &api.BatchAction_Deploy{
   387  						Deploy: &api.DeployRequest{
   388  							Environment:  "dev",
   389  							Application:  "myapp",
   390  							Version:      666,
   391  							LockBehavior: 0,
   392  						},
   393  					},
   394  				}},
   395  			ExpectedResponse: nil,
   396  			ExpectedError: &repository.TransformerBatchApplyError{
   397  				Index:            0,
   398  				TransformerError: errMatcher{"deployment failed: could not open manifest for app myapp with release 666 on env dev 'applications/myapp/releases/666/environments/dev/manifests.yaml': file does not exist"},
   399  			},
   400  		},
   401  		{
   402  			Name:  "create release endpoint fails app validity check",
   403  			Setup: []repository.Transformer{},
   404  			Batch: []*api.BatchAction{
   405  				{
   406  					Action: &api.BatchAction_CreateRelease{
   407  						CreateRelease: &api.CreateReleaseRequest{
   408  							Environment:    "dev",
   409  							Application:    "myappIsWayTooLongDontYouThink",
   410  							Team:           "team1",
   411  							Manifests:      nil,
   412  							Version:        666,
   413  							SourceCommitId: "1",
   414  							SourceAuthor:   "2",
   415  							SourceMessage:  "3",
   416  							SourceRepoUrl:  "4",
   417  						},
   418  					},
   419  				},
   420  			},
   421  			ExpectedResponse: &api.BatchResponse{
   422  				Results: []*api.BatchResult{
   423  					{
   424  						Result: &api.BatchResult_CreateReleaseResponse{
   425  							CreateReleaseResponse: &api.CreateReleaseResponse{
   426  								Response: &api.CreateReleaseResponse_TooLong{
   427  									TooLong: &api.CreateReleaseResponseAppNameTooLong{
   428  										AppName: "myappIsWayTooLongDontYouThink",
   429  										RegExp:  "\\A[a-z0-9]+(?:-[a-z0-9]+)*\\z",
   430  										MaxLen:  39,
   431  									},
   432  								},
   433  							},
   434  						},
   435  					},
   436  				},
   437  			},
   438  		},
   439  	}
   440  	for _, tc := range tcs {
   441  		tc := tc
   442  		t.Run(tc.Name, func(t *testing.T) {
   443  			repo, err := setupRepositoryTest(t)
   444  			if err != nil {
   445  				t.Fatal(err)
   446  			}
   447  			for _, tr := range tc.Setup {
   448  				if err := repo.Apply(testutil.MakeTestContext(), tr); err != nil {
   449  					t.Fatal(err)
   450  				}
   451  			}
   452  			svc := &BatchServer{
   453  				Repository: repo,
   454  			}
   455  			response, processErr := svc.ProcessBatch(
   456  				testutil.MakeTestContext(),
   457  				&api.BatchRequest{
   458  					Actions: tc.Batch,
   459  				},
   460  			)
   461  			if diff := cmp.Diff(tc.ExpectedError, processErr, cmpopts.EquateErrors()); diff != "" {
   462  				t.Errorf("error mismatch (-want, +got):\n%s", diff)
   463  			}
   464  			if diff := cmp.Diff(tc.ExpectedResponse, response, protocmp.Transform()); diff != "" {
   465  				t.Fatalf("response mismatch, diff (-want, +got):\n%s", diff)
   466  			}
   467  		})
   468  	}
   469  }
   470  
   471  func TestBatchServiceLimit(t *testing.T) {
   472  	transformers := []repository.Transformer{
   473  		&repository.CreateEnvironment{
   474  			Environment: "production",
   475  			Config:      config.EnvironmentConfig{Upstream: &config.EnvironmentConfigUpstream{Latest: true}},
   476  		},
   477  		&repository.CreateApplicationVersion{
   478  			Application: "test",
   479  			Manifests: map[string]string{
   480  				"production": "manifest",
   481  			},
   482  		},
   483  		&repository.CreateApplicationVersion{
   484  			Application: "test",
   485  			Manifests: map[string]string{
   486  				"production": "manifest2",
   487  			},
   488  		},
   489  	}
   490  	var two uint64 = 2
   491  	tcs := []struct {
   492  		Name            string
   493  		Batch           []*api.BatchAction
   494  		Setup           []repository.Transformer
   495  		ShouldSucceed   bool
   496  		ExpectedVersion *uint64
   497  	}{
   498  		{
   499  			Name:            "exactly the maximum number of actions",
   500  			Setup:           transformers,
   501  			ShouldSucceed:   true,
   502  			Batch:           getNBatchActions(maxBatchActions),
   503  			ExpectedVersion: &two,
   504  		},
   505  		{
   506  			Name:            "more than the maximum number of actions",
   507  			Setup:           transformers,
   508  			ShouldSucceed:   false,
   509  			Batch:           getNBatchActions(maxBatchActions + 1), // more than max
   510  			ExpectedVersion: nil,
   511  		},
   512  	}
   513  	for _, tc := range tcs {
   514  		tc := tc
   515  		t.Run(tc.Name, func(t *testing.T) {
   516  			repo, err := setupRepositoryTest(t)
   517  			if err != nil {
   518  				t.Fatal(err)
   519  			}
   520  			for _, tr := range tc.Setup {
   521  				if err := repo.Apply(testutil.MakeTestContext(), tr); err != nil {
   522  					t.Fatal(err)
   523  				}
   524  			}
   525  			svc := &BatchServer{
   526  				Repository: repo,
   527  			}
   528  			_, err = svc.ProcessBatch(
   529  				testutil.MakeTestContext(),
   530  				&api.BatchRequest{
   531  					Actions: tc.Batch,
   532  				},
   533  			)
   534  			if !tc.ShouldSucceed {
   535  				if err == nil {
   536  					t.Fatal("expected an error but got none")
   537  				}
   538  				s, ok := status.FromError(err)
   539  				if !ok {
   540  					t.Fatalf("error is not a status error, got: %#v", err)
   541  				}
   542  				expectedMessage := fmt.Sprintf("cannot process batch: too many actions. limit is %d", maxBatchActions)
   543  				if s.Message() != expectedMessage {
   544  					t.Errorf("invalid error message: expected %q, actual: %q", expectedMessage, s.Message())
   545  				}
   546  			} else {
   547  				if err != nil {
   548  					t.Fatal(err)
   549  				}
   550  				version, err := svc.Repository.State().GetEnvironmentApplicationVersion("production", "test")
   551  				if err != nil {
   552  					t.Fatal(err)
   553  				}
   554  				if version == nil {
   555  					t.Errorf("unexpected version: expected %d, actual: %d", *tc.ExpectedVersion, version)
   556  				}
   557  				if *version != *tc.ExpectedVersion {
   558  					t.Errorf("unexpected version: expected %d, actual: %d", *tc.ExpectedVersion, *version)
   559  				}
   560  			}
   561  		})
   562  	}
   563  }
   564  
   565  func setupRepositoryTest(t *testing.T) (repository.Repository, error) {
   566  	t.Parallel()
   567  	dir := t.TempDir()
   568  	remoteDir := path.Join(dir, "remote")
   569  	localDir := path.Join(dir, "local")
   570  	cmd := exec.Command("git", "init", "--bare", remoteDir)
   571  	cmd.Start()
   572  	cmd.Wait()
   573  	t.Logf("test created dir: %s", localDir)
   574  	repo, err := repository.New(
   575  		testutil.MakeTestContext(),
   576  		repository.RepositoryConfig{
   577  			URL:                    remoteDir,
   578  			Path:                   localDir,
   579  			CommitterEmail:         "kuberpult@freiheit.com",
   580  			CommitterName:          "kuberpult",
   581  			EnvironmentConfigsPath: filepath.Join(remoteDir, "..", "environment_configs.json"),
   582  		},
   583  	)
   584  	if err != nil {
   585  		t.Fatal(err)
   586  	}
   587  	return repo, nil
   588  }
   589  
   590  func TestReleaseTrain(t *testing.T) {
   591  	tcs := []struct {
   592  		Name             string
   593  		Setup            []repository.Transformer
   594  		Request          *api.BatchRequest
   595  		ExpectedResponse *api.BatchResponse
   596  	}{
   597  		{
   598  			Name: "Get Upstream env and TargetEnv",
   599  			Setup: []repository.Transformer{
   600  				&repository.CreateEnvironment{
   601  					Environment: "acceptance",
   602  					Config:      config.EnvironmentConfig{Upstream: &config.EnvironmentConfigUpstream{Environment: "production"}},
   603  				},
   604  				&repository.CreateApplicationVersion{
   605  					Application: "test",
   606  					Manifests: map[string]string{
   607  						"acceptance": "manifest",
   608  					},
   609  				},
   610  			},
   611  			Request: &api.BatchRequest{
   612  				Actions: []*api.BatchAction{
   613  					{
   614  						Action: &api.BatchAction_ReleaseTrain{
   615  							ReleaseTrain: &api.ReleaseTrainRequest{
   616  								Target: "acceptance",
   617  
   618  								Team: "team",
   619  							},
   620  						},
   621  					},
   622  				},
   623  			},
   624  			ExpectedResponse: &api.BatchResponse{
   625  				Results: []*api.BatchResult{
   626  					{
   627  						Result: &api.BatchResult_ReleaseTrain{
   628  							ReleaseTrain: &api.ReleaseTrainResponse{
   629  								Target: "acceptance",
   630  								Team:   "team",
   631  							},
   632  						},
   633  					},
   634  				},
   635  			},
   636  		},
   637  		{
   638  			Name: "Get Upstream (latest) and TargetEnv",
   639  			Setup: []repository.Transformer{
   640  				&repository.CreateEnvironment{
   641  					Environment: "acceptance",
   642  					Config:      config.EnvironmentConfig{Upstream: &config.EnvironmentConfigUpstream{Latest: true}},
   643  				},
   644  				&repository.CreateApplicationVersion{
   645  					Application: "test",
   646  					Manifests: map[string]string{
   647  						"acceptance": "manifest",
   648  					},
   649  				},
   650  			},
   651  			Request: &api.BatchRequest{
   652  				Actions: []*api.BatchAction{
   653  					{
   654  						Action: &api.BatchAction_ReleaseTrain{
   655  							ReleaseTrain: &api.ReleaseTrainRequest{
   656  								Target: "acceptance",
   657  
   658  								Team: "team",
   659  							},
   660  						},
   661  					},
   662  				},
   663  			},
   664  			ExpectedResponse: &api.BatchResponse{
   665  				Results: []*api.BatchResult{
   666  					{
   667  						Result: &api.BatchResult_ReleaseTrain{
   668  							ReleaseTrain: &api.ReleaseTrainResponse{
   669  								Target: "acceptance",
   670  								Team:   "team",
   671  							},
   672  						},
   673  					},
   674  				},
   675  			},
   676  		},
   677  	}
   678  	for _, tc := range tcs {
   679  		tc := tc
   680  		t.Run(tc.Name, func(t *testing.T) {
   681  			repo, err := setupRepositoryTest(t)
   682  			if err != nil {
   683  				t.Fatal(err)
   684  			}
   685  			for _, tr := range tc.Setup {
   686  				if err := repo.Apply(testutil.MakeTestContext(), tr); err != nil {
   687  					t.Fatal(err)
   688  				}
   689  			}
   690  			svc := &BatchServer{
   691  				Repository: repo,
   692  			}
   693  			resp, err := svc.ProcessBatch(
   694  				testutil.MakeTestContext(),
   695  				tc.Request,
   696  			)
   697  			if err != nil {
   698  				t.Errorf("unexpected error: %q", err)
   699  			}
   700  			if d := cmp.Diff(tc.ExpectedResponse, resp, protocmp.Transform()); d != "" {
   701  				t.Errorf("batch response mismatch: %s", d)
   702  			}
   703  		})
   704  	}
   705  }
   706  
   707  func TestCreateEnvironmentTrain(t *testing.T) {
   708  	tcs := []struct {
   709  		Name                 string
   710  		Setup                []repository.Transformer
   711  		Request              *api.BatchRequest
   712  		ExpectedResponse     *api.BatchResponse
   713  		ExpectedEnvironments map[string]config.EnvironmentConfig
   714  	}{
   715  		{
   716  			Name:  "Minimal test case",
   717  			Setup: []repository.Transformer{},
   718  			Request: &api.BatchRequest{
   719  				Actions: []*api.BatchAction{
   720  					{
   721  						Action: &api.BatchAction_CreateEnvironment{
   722  							CreateEnvironment: &api.CreateEnvironmentRequest{
   723  								Environment: "env",
   724  							},
   725  						},
   726  					},
   727  				},
   728  			},
   729  			ExpectedResponse: &api.BatchResponse{
   730  				Results: []*api.BatchResult{
   731  					nil,
   732  				},
   733  			},
   734  			ExpectedEnvironments: map[string]config.EnvironmentConfig{
   735  				"env": config.EnvironmentConfig{},
   736  			},
   737  		},
   738  		{
   739  			Name:  "With upstream latest",
   740  			Setup: []repository.Transformer{},
   741  			Request: &api.BatchRequest{
   742  				Actions: []*api.BatchAction{
   743  					{
   744  						Action: &api.BatchAction_CreateEnvironment{
   745  							CreateEnvironment: &api.CreateEnvironmentRequest{
   746  								Environment: "env",
   747  								Config: &api.EnvironmentConfig{
   748  									Upstream: &api.EnvironmentConfig_Upstream{
   749  										Latest: ptr.Bool(true),
   750  									},
   751  								},
   752  							},
   753  						},
   754  					},
   755  				},
   756  			},
   757  			ExpectedResponse: &api.BatchResponse{
   758  				Results: []*api.BatchResult{
   759  					nil,
   760  				},
   761  			},
   762  			ExpectedEnvironments: map[string]config.EnvironmentConfig{
   763  				"env": config.EnvironmentConfig{
   764  					Upstream: &config.EnvironmentConfigUpstream{Latest: true},
   765  				},
   766  			},
   767  		},
   768  		{
   769  			Name:  "With upstream env",
   770  			Setup: []repository.Transformer{},
   771  			Request: &api.BatchRequest{
   772  				Actions: []*api.BatchAction{
   773  					{
   774  						Action: &api.BatchAction_CreateEnvironment{
   775  							CreateEnvironment: &api.CreateEnvironmentRequest{
   776  								Environment: "env",
   777  								Config: &api.EnvironmentConfig{
   778  									Upstream: &api.EnvironmentConfig_Upstream{
   779  										Environment: ptr.FromString("other-env"),
   780  									},
   781  								},
   782  							},
   783  						},
   784  					},
   785  				},
   786  			},
   787  			ExpectedResponse: &api.BatchResponse{
   788  				Results: []*api.BatchResult{
   789  					nil,
   790  				},
   791  			},
   792  			ExpectedEnvironments: map[string]config.EnvironmentConfig{
   793  				"env": config.EnvironmentConfig{
   794  					Upstream: &config.EnvironmentConfigUpstream{Environment: "other-env"},
   795  				},
   796  			},
   797  		},
   798  		{
   799  			Name:  "With minimal argocd config",
   800  			Setup: []repository.Transformer{},
   801  			Request: &api.BatchRequest{
   802  				Actions: []*api.BatchAction{
   803  					{
   804  						Action: &api.BatchAction_CreateEnvironment{
   805  							CreateEnvironment: &api.CreateEnvironmentRequest{
   806  								Environment: "env",
   807  								Config: &api.EnvironmentConfig{
   808  									Argocd: &api.EnvironmentConfig_ArgoCD{},
   809  								},
   810  							},
   811  						},
   812  					},
   813  				},
   814  			},
   815  			ExpectedResponse: &api.BatchResponse{
   816  				Results: []*api.BatchResult{
   817  					nil,
   818  				},
   819  			},
   820  			ExpectedEnvironments: map[string]config.EnvironmentConfig{
   821  				"env": config.EnvironmentConfig{
   822  					ArgoCd: &config.EnvironmentConfigArgoCd{},
   823  				},
   824  			},
   825  		},
   826  		{
   827  			Name:  "With full argocd config",
   828  			Setup: []repository.Transformer{},
   829  			Request: &api.BatchRequest{
   830  				Actions: []*api.BatchAction{
   831  					{
   832  						Action: &api.BatchAction_CreateEnvironment{
   833  							CreateEnvironment: &api.CreateEnvironmentRequest{
   834  								Environment: "env",
   835  								Config: &api.EnvironmentConfig{
   836  									Argocd: &api.EnvironmentConfig_ArgoCD{
   837  										Destination: &api.EnvironmentConfig_ArgoCD_Destination{
   838  											Name:                 "name",
   839  											Server:               "server",
   840  											Namespace:            ptr.FromString("namespace"),
   841  											AppProjectNamespace:  ptr.FromString("app-project-namespace"),
   842  											ApplicationNamespace: ptr.FromString("app-namespace"),
   843  										},
   844  										SyncWindows: []*api.EnvironmentConfig_ArgoCD_SyncWindows{
   845  											&api.EnvironmentConfig_ArgoCD_SyncWindows{
   846  												Schedule:     "schedule",
   847  												Duration:     "duration",
   848  												Kind:         "kind",
   849  												Applications: []string{"applications"},
   850  											},
   851  										},
   852  										AccessList: []*api.EnvironmentConfig_ArgoCD_AccessEntry{
   853  											&api.EnvironmentConfig_ArgoCD_AccessEntry{
   854  												Group: "group",
   855  												Kind:  "kind",
   856  											},
   857  										},
   858  										SyncOptions: []string{"sync-option"},
   859  										IgnoreDifferences: []*api.EnvironmentConfig_ArgoCD_IgnoreDifferences{
   860  											{
   861  												Group:                 "group",
   862  												Kind:                  "kind",
   863  												Name:                  "name",
   864  												Namespace:             "namespace",
   865  												JsonPointers:          []string{"/json"},
   866  												JqPathExpressions:     []string{".jq"},
   867  												ManagedFieldsManagers: []string{"manager"},
   868  											},
   869  										},
   870  									},
   871  								},
   872  							},
   873  						},
   874  					},
   875  				},
   876  			},
   877  			ExpectedResponse: &api.BatchResponse{
   878  				Results: []*api.BatchResult{
   879  					nil,
   880  				},
   881  			},
   882  			ExpectedEnvironments: map[string]config.EnvironmentConfig{
   883  				"env": config.EnvironmentConfig{
   884  					ArgoCd: &config.EnvironmentConfigArgoCd{
   885  						Destination: config.ArgoCdDestination{
   886  							Name:                 "name",
   887  							Server:               "server",
   888  							Namespace:            ptr.FromString("namespace"),
   889  							AppProjectNamespace:  ptr.FromString("app-project-namespace"),
   890  							ApplicationNamespace: ptr.FromString("app-namespace"),
   891  						},
   892  						SyncWindows: []config.ArgoCdSyncWindow{
   893  							{
   894  								Schedule: "schedule",
   895  								Duration: "duration",
   896  								Kind:     "kind",
   897  								Apps:     []string{"applications"},
   898  							},
   899  						},
   900  						ClusterResourceWhitelist: []config.AccessEntry{{Group: "group", Kind: "kind"}},
   901  						SyncOptions:              []string{"sync-option"},
   902  						IgnoreDifferences: []config.ArgoCdIgnoreDifference{
   903  							{
   904  								Group:                 "group",
   905  								Kind:                  "kind",
   906  								Name:                  "name",
   907  								Namespace:             "namespace",
   908  								JSONPointers:          []string{"/json"},
   909  								JqPathExpressions:     []string{".jq"},
   910  								ManagedFieldsManagers: []string{"manager"},
   911  							},
   912  						},
   913  					},
   914  				},
   915  			},
   916  		},
   917  	}
   918  	for _, tc := range tcs {
   919  		tc := tc
   920  		t.Run(tc.Name, func(t *testing.T) {
   921  			repo, err := setupRepositoryTest(t)
   922  			if err != nil {
   923  				t.Fatal(err)
   924  			}
   925  			for _, tr := range tc.Setup {
   926  				if err := repo.Apply(testutil.MakeTestContext(), tr); err != nil {
   927  					t.Fatal(err)
   928  				}
   929  			}
   930  			svc := &BatchServer{
   931  				Repository: repo,
   932  			}
   933  			resp, err := svc.ProcessBatch(
   934  				testutil.MakeTestContext(),
   935  				tc.Request,
   936  			)
   937  			if err != nil {
   938  				t.Errorf("unexpected error: %q", err)
   939  			}
   940  			if d := cmp.Diff(tc.ExpectedResponse, resp, protocmp.Transform()); d != "" {
   941  				t.Errorf("batch response mismatch: %s", d)
   942  			}
   943  			envs, err := repo.State().GetEnvironmentConfigs()
   944  			if err != nil {
   945  				t.Errorf("unexpected error: %q", err)
   946  			}
   947  			if d := cmp.Diff(tc.ExpectedEnvironments, envs); d != "" {
   948  				t.Errorf("batch response mismatch: %s", d)
   949  			}
   950  		})
   951  	}
   952  }