github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/rollout-service/pkg/service/broadcast_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  	"testing"
    23  	"time"
    24  
    25  	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    26  	"github.com/argoproj/gitops-engine/pkg/health"
    27  	"github.com/argoproj/gitops-engine/pkg/sync/common"
    28  	api "github.com/freiheit-com/kuberpult/pkg/api/v1"
    29  	"github.com/freiheit-com/kuberpult/services/rollout-service/pkg/versions"
    30  	"github.com/google/go-cmp/cmp"
    31  	"google.golang.org/grpc"
    32  	"google.golang.org/protobuf/proto"
    33  	"google.golang.org/protobuf/testing/protocmp"
    34  )
    35  
    36  type testSrv struct {
    37  	ch  chan *api.StreamStatusResponse
    38  	ctx context.Context
    39  	grpc.ServerStream
    40  }
    41  
    42  func (t *testSrv) Send(resp *api.StreamStatusResponse) error {
    43  	t.ch <- resp
    44  	return nil
    45  }
    46  
    47  func (t *testSrv) Context() context.Context {
    48  	return t.ctx
    49  }
    50  
    51  type errSrv struct {
    52  	err error
    53  	ctx context.Context
    54  	grpc.ServerStream
    55  }
    56  
    57  func (t *errSrv) Send(_ *api.StreamStatusResponse) error {
    58  	return t.err
    59  }
    60  
    61  func (t *errSrv) Context() context.Context {
    62  	return t.ctx
    63  }
    64  
    65  func TestBroadcast(t *testing.T) {
    66  	t.Parallel()
    67  	var (
    68  		RolloutStatusSuccesful   = api.RolloutStatus_ROLLOUT_STATUS_SUCCESFUL
    69  		RolloutStatusProgressing = api.RolloutStatus_ROLLOUT_STATUS_PROGRESSING
    70  		RolloutStatusError       = api.RolloutStatus_ROLLOUT_STATUS_ERROR
    71  		RolloutStatusUnknown     = api.RolloutStatus_ROLLOUT_STATUS_UNKNOWN
    72  		RolloutStatusUnhealthy   = api.RolloutStatus_ROLLOUT_STATUS_UNHEALTHY
    73  		RolloutStatusPending     = api.RolloutStatus_ROLLOUT_STATUS_PENDING
    74  	)
    75  	type step struct {
    76  		ArgoEvent    *ArgoEvent
    77  		VersionEvent *versions.KuberpultEvent
    78  
    79  		ExpectStatus *api.RolloutStatus
    80  	}
    81  
    82  	application := func(s step) string {
    83  		if s.ArgoEvent != nil {
    84  			return s.ArgoEvent.Application
    85  		}
    86  		return s.VersionEvent.Application
    87  	}
    88  	environment := func(s step) string {
    89  		if s.ArgoEvent != nil {
    90  			return s.ArgoEvent.Environment
    91  		}
    92  		return s.VersionEvent.Environment
    93  	}
    94  
    95  	tcs := []struct {
    96  		Name  string
    97  		Steps []step
    98  	}{
    99  		{
   100  			Name: "simple case",
   101  			Steps: []step{
   102  				{
   103  					ArgoEvent: &ArgoEvent{
   104  						Application:      "foo",
   105  						Environment:      "bar",
   106  						Version:          &versions.VersionInfo{Version: 1},
   107  						SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   108  						HealthStatusCode: health.HealthStatusHealthy,
   109  					},
   110  				},
   111  				{
   112  					VersionEvent: &versions.KuberpultEvent{
   113  						Application: "foo",
   114  						Environment: "bar",
   115  						Version:     &versions.VersionInfo{Version: 1},
   116  					},
   117  
   118  					ExpectStatus: &RolloutStatusSuccesful,
   119  				},
   120  			},
   121  		},
   122  		{
   123  			Name: "missing argo app",
   124  			Steps: []step{
   125  				{
   126  					VersionEvent: &versions.KuberpultEvent{
   127  						Application: "foo",
   128  						Environment: "bar",
   129  						Version:     &versions.VersionInfo{Version: 2},
   130  					},
   131  
   132  					ExpectStatus: &RolloutStatusUnknown,
   133  				},
   134  			},
   135  		},
   136  		{
   137  			Name: "missing version in argo event",
   138  			Steps: []step{
   139  				{
   140  					ArgoEvent: &ArgoEvent{
   141  						Application:      "foo",
   142  						Environment:      "bar",
   143  						Version:          &versions.VersionInfo{Version: 1},
   144  						SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   145  						HealthStatusCode: health.HealthStatusHealthy,
   146  					},
   147  
   148  					ExpectStatus: &RolloutStatusUnknown,
   149  				},
   150  				{
   151  					ArgoEvent: &ArgoEvent{
   152  						Application:      "foo",
   153  						Environment:      "bar",
   154  						Version:          nil,
   155  						SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   156  						HealthStatusCode: health.HealthStatusHealthy,
   157  					},
   158  
   159  					ExpectStatus: &RolloutStatusUnknown,
   160  				},
   161  			},
   162  		},
   163  		{
   164  			Name: "app syncing and becomming healthy",
   165  			Steps: []step{
   166  				{
   167  					VersionEvent: &versions.KuberpultEvent{
   168  						Application: "foo",
   169  						Environment: "bar",
   170  						Version:     &versions.VersionInfo{Version: 1},
   171  					},
   172  
   173  					ExpectStatus: &RolloutStatusUnknown,
   174  				},
   175  				{
   176  					ArgoEvent: &ArgoEvent{
   177  						Application:      "foo",
   178  						Environment:      "bar",
   179  						Version:          &versions.VersionInfo{Version: 1},
   180  						SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   181  						HealthStatusCode: health.HealthStatusHealthy,
   182  					},
   183  
   184  					ExpectStatus: &RolloutStatusSuccesful,
   185  				},
   186  				{
   187  					VersionEvent: &versions.KuberpultEvent{
   188  						Application: "foo",
   189  						Environment: "bar",
   190  						Version:     &versions.VersionInfo{Version: 2},
   191  					},
   192  
   193  					ExpectStatus: &RolloutStatusPending,
   194  				},
   195  				{
   196  					ArgoEvent: &ArgoEvent{
   197  						Application:      "foo",
   198  						Environment:      "bar",
   199  						Version:          &versions.VersionInfo{Version: 2},
   200  						SyncStatusCode:   v1alpha1.SyncStatusCodeOutOfSync,
   201  						HealthStatusCode: health.HealthStatusHealthy,
   202  					},
   203  
   204  					ExpectStatus: &RolloutStatusProgressing,
   205  				},
   206  				{
   207  					ArgoEvent: &ArgoEvent{
   208  						Application:      "foo",
   209  						Environment:      "bar",
   210  						Version:          &versions.VersionInfo{Version: 2},
   211  						SyncStatusCode:   v1alpha1.SyncStatusCodeOutOfSync,
   212  						HealthStatusCode: health.HealthStatusProgressing,
   213  					},
   214  
   215  					ExpectStatus: nil,
   216  				},
   217  				{
   218  					ArgoEvent: &ArgoEvent{
   219  						Application:      "foo",
   220  						Environment:      "bar",
   221  						Version:          &versions.VersionInfo{Version: 2},
   222  						SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   223  						HealthStatusCode: health.HealthStatusHealthy,
   224  					},
   225  
   226  					ExpectStatus: &RolloutStatusSuccesful,
   227  				},
   228  			},
   229  		},
   230  		{
   231  			Name: "app becomming unhealthy and recovers",
   232  			Steps: []step{
   233  				{
   234  					VersionEvent: &versions.KuberpultEvent{
   235  						Application: "foo",
   236  						Environment: "bar",
   237  						Version:     &versions.VersionInfo{Version: 1},
   238  					},
   239  
   240  					ExpectStatus: &RolloutStatusUnknown,
   241  				},
   242  				{
   243  					ArgoEvent: &ArgoEvent{
   244  						Application:      "foo",
   245  						Environment:      "bar",
   246  						Version:          &versions.VersionInfo{Version: 1},
   247  						SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   248  						HealthStatusCode: health.HealthStatusHealthy,
   249  					},
   250  
   251  					ExpectStatus: &RolloutStatusSuccesful,
   252  				},
   253  				{
   254  					ArgoEvent: &ArgoEvent{
   255  						Application:      "foo",
   256  						Environment:      "bar",
   257  						Version:          &versions.VersionInfo{Version: 1},
   258  						SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   259  						HealthStatusCode: health.HealthStatusDegraded,
   260  					},
   261  
   262  					ExpectStatus: &RolloutStatusUnhealthy,
   263  				},
   264  				{
   265  					ArgoEvent: &ArgoEvent{
   266  						Application:      "foo",
   267  						Environment:      "bar",
   268  						Version:          &versions.VersionInfo{Version: 1},
   269  						SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   270  						HealthStatusCode: health.HealthStatusHealthy,
   271  					},
   272  
   273  					ExpectStatus: &RolloutStatusSuccesful,
   274  				},
   275  			},
   276  		},
   277  		{
   278  			Name: "rollout fails",
   279  			Steps: []step{
   280  				{
   281  					ArgoEvent: &ArgoEvent{
   282  						Application:      "foo",
   283  						Environment:      "bar",
   284  						Version:          &versions.VersionInfo{Version: 1},
   285  						SyncStatusCode:   v1alpha1.SyncStatusCodeOutOfSync,
   286  						HealthStatusCode: health.HealthStatusHealthy,
   287  						OperationState: &v1alpha1.OperationState{
   288  							Phase: common.OperationFailed,
   289  						},
   290  					},
   291  
   292  					ExpectStatus: &RolloutStatusError,
   293  				},
   294  			},
   295  		},
   296  		{
   297  			Name: "rollout errors",
   298  			Steps: []step{
   299  				{
   300  					ArgoEvent: &ArgoEvent{
   301  						Application:      "foo",
   302  						Environment:      "bar",
   303  						Version:          &versions.VersionInfo{Version: 1},
   304  						SyncStatusCode:   v1alpha1.SyncStatusCodeOutOfSync,
   305  						HealthStatusCode: health.HealthStatusHealthy,
   306  						OperationState: &v1alpha1.OperationState{
   307  							Phase: common.OperationError,
   308  						},
   309  					},
   310  
   311  					ExpectStatus: &RolloutStatusError,
   312  				},
   313  			},
   314  		},
   315  		{
   316  			Name: "healthy app switches to pending when a new version in kuberpult is deployed",
   317  			Steps: []step{
   318  				{
   319  					VersionEvent: &versions.KuberpultEvent{
   320  						Application: "foo",
   321  						Environment: "bar",
   322  						Version:     &versions.VersionInfo{Version: 1},
   323  					},
   324  
   325  					ExpectStatus: &RolloutStatusUnknown,
   326  				},
   327  				{
   328  					ArgoEvent: &ArgoEvent{
   329  						Application:      "foo",
   330  						Environment:      "bar",
   331  						Version:          &versions.VersionInfo{Version: 1},
   332  						SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   333  						HealthStatusCode: health.HealthStatusHealthy,
   334  					},
   335  
   336  					ExpectStatus: &RolloutStatusSuccesful,
   337  				},
   338  				{
   339  					VersionEvent: &versions.KuberpultEvent{
   340  						Application: "foo",
   341  						Environment: "bar",
   342  						Version:     &versions.VersionInfo{Version: 2},
   343  					},
   344  
   345  					ExpectStatus: &RolloutStatusPending,
   346  				},
   347  				{
   348  					ArgoEvent: &ArgoEvent{
   349  						Application:      "foo",
   350  						Environment:      "bar",
   351  						Version:          &versions.VersionInfo{Version: 2},
   352  						SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   353  						HealthStatusCode: health.HealthStatusHealthy,
   354  					},
   355  
   356  					ExpectStatus: &RolloutStatusSuccesful,
   357  				},
   358  			},
   359  		},
   360  	}
   361  
   362  	for _, tc := range tcs {
   363  		tc := tc
   364  		t.Run(tc.Name+" (streaming)", func(t *testing.T) {
   365  			bc := New()
   366  			ctx, cancel := context.WithCancel(context.Background())
   367  			ch := make(chan *api.StreamStatusResponse)
   368  			srv := testSrv{ctx: ctx, ch: ch}
   369  			go bc.StreamStatus(&api.StreamStatusRequest{}, &srv)
   370  			for i, s := range tc.Steps {
   371  				if s.ArgoEvent != nil {
   372  					bc.ProcessArgoEvent(context.Background(), *s.ArgoEvent)
   373  				} else if s.VersionEvent != nil {
   374  					bc.ProcessKuberpultEvent(context.Background(), *s.VersionEvent)
   375  				}
   376  				if s.ExpectStatus != nil {
   377  					resp := <-ch
   378  					if resp.Application != application(s) {
   379  						t.Errorf("wrong application received in step %d: expected %q, got %q", i, application(s), resp.Application)
   380  					}
   381  					if resp.Environment != environment(s) {
   382  						t.Errorf("wrong environment received in step %d: expected %q, got %q", i, environment(s), resp.Environment)
   383  					}
   384  					if resp.RolloutStatus != *s.ExpectStatus {
   385  						t.Errorf("wrong status received in step %d: expected %q, got %q", i, s.ExpectStatus, resp.RolloutStatus)
   386  					}
   387  				} else {
   388  					select {
   389  					case resp := <-ch:
   390  						t.Errorf("didn't expect status update but got %#v", resp)
   391  					default:
   392  					}
   393  				}
   394  			}
   395  			cancel()
   396  		})
   397  		t.Run(tc.Name+" (once)", func(t *testing.T) {
   398  			bc := New()
   399  			for i, s := range tc.Steps {
   400  				if s.ArgoEvent != nil {
   401  					bc.ProcessArgoEvent(context.Background(), *s.ArgoEvent)
   402  				} else if s.VersionEvent != nil {
   403  					bc.ProcessKuberpultEvent(context.Background(), *s.VersionEvent)
   404  				}
   405  				if s.ExpectStatus != nil {
   406  					ctx, cancel := context.WithCancel(context.Background())
   407  					ch := make(chan *api.StreamStatusResponse, 1)
   408  					srv := testSrv{ctx: ctx, ch: ch}
   409  					go bc.StreamStatus(&api.StreamStatusRequest{}, &srv)
   410  					resp := <-ch
   411  					cancel()
   412  					if resp.Application != application(s) {
   413  						t.Errorf("wrong application received in step %d: expected %q, got %q", i, application(s), resp.Application)
   414  					}
   415  					if resp.Environment != environment(s) {
   416  						t.Errorf("wrong environment received in step %d: expected %q, got %q", i, environment(s), resp.Environment)
   417  					}
   418  					if resp.RolloutStatus != *s.ExpectStatus {
   419  						t.Errorf("wrong status received in step %d: expected %q, got %q", i, s.ExpectStatus, resp.RolloutStatus)
   420  					}
   421  				}
   422  			}
   423  		})
   424  		t.Run(tc.Name+" (get)", func(t *testing.T) {
   425  			bc := New()
   426  			lastStatus := RolloutStatusUnknown
   427  			for i, s := range tc.Steps {
   428  				if s.ArgoEvent != nil {
   429  					bc.ProcessArgoEvent(context.Background(), *s.ArgoEvent)
   430  				} else if s.VersionEvent != nil {
   431  					bc.ProcessKuberpultEvent(context.Background(), *s.VersionEvent)
   432  				}
   433  
   434  				ctx, cancel := context.WithCancel(context.Background())
   435  				resp, err := bc.GetStatus(ctx, &api.GetStatusRequest{})
   436  				cancel()
   437  				if err != nil {
   438  					t.Errorf("didn't expect an error but got %q", err)
   439  				}
   440  
   441  				if s.ExpectStatus != nil {
   442  					lastStatus = *s.ExpectStatus
   443  				}
   444  				if resp.Status != lastStatus {
   445  					t.Errorf("wrong status received in step %d: expected %q, got %q", i, lastStatus, resp.Status)
   446  				}
   447  
   448  				if lastStatus == RolloutStatusSuccesful {
   449  					// Apps with successful state are excluded
   450  					if len(resp.Applications) != 0 {
   451  						t.Errorf("expected no applications but got %d", len(resp.Applications))
   452  					}
   453  					continue
   454  				}
   455  				app := resp.Applications[0]
   456  				if app.Application != application(s) {
   457  					t.Errorf("wrong application received in step %d: expected %q, got %q", i, application(s), app.Application)
   458  				}
   459  				if app.Environment != environment(s) {
   460  					t.Errorf("wrong environment received in step %d: expected %q, got %q", i, environment(s), app.Environment)
   461  				}
   462  				if app.RolloutStatus != lastStatus {
   463  					t.Errorf("wrong status received in step %d: expected %q, got %q", i, lastStatus, app.RolloutStatus)
   464  				}
   465  			}
   466  		})
   467  
   468  	}
   469  }
   470  
   471  func TestBroadcastDoesntGetStuck(t *testing.T) {
   472  	t.Parallel()
   473  	tcs := []struct {
   474  		Name   string
   475  		Events uint
   476  	}{
   477  		{
   478  			Name:   "200 events",
   479  			Events: 200,
   480  		},
   481  	}
   482  	for _, tc := range tcs {
   483  		tc := tc
   484  		t.Run(tc.Name, func(t *testing.T) {
   485  			bc := New()
   486  			// srv1 will just be blocked
   487  			ctx1, cancel1 := context.WithCancel(context.Background())
   488  			ch1 := make(chan *api.StreamStatusResponse, 200)
   489  			ech1 := make(chan error, 1)
   490  			srv1 := testSrv{ctx: ctx1, ch: ch1}
   491  			go func() {
   492  				ech1 <- bc.StreamStatus(&api.StreamStatusRequest{}, &srv1)
   493  			}()
   494  			defer cancel1()
   495  			// srv2 will actually get consumed
   496  			ctx2, cancel2 := context.WithCancel(context.Background())
   497  			ch2 := make(chan *api.StreamStatusResponse)
   498  			ech2 := make(chan error, 1)
   499  			srv2 := testSrv{ctx: ctx2, ch: ch2}
   500  			go func() {
   501  				ech2 <- bc.StreamStatus(&api.StreamStatusRequest{}, &srv2)
   502  			}()
   503  			defer cancel2()
   504  			// srv3 will just return an error
   505  			ctx3, cancel3 := context.WithCancel(context.Background())
   506  			ech3 := make(chan error, 1)
   507  			testErr := fmt.Errorf("some error")
   508  			srv3 := errSrv{ctx: ctx3, err: testErr}
   509  			go func() {
   510  				ech3 <- bc.StreamStatus(&api.StreamStatusRequest{}, &srv3)
   511  			}()
   512  			defer cancel3()
   513  
   514  			for i := uint(0); i < tc.Events; i += 1 {
   515  				app := fmt.Sprintf("app-%d", i)
   516  				bc.ProcessArgoEvent(context.Background(), ArgoEvent{
   517  					Application:      app,
   518  					Environment:      "doesntmatter",
   519  					HealthStatusCode: health.HealthStatusHealthy,
   520  					SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   521  					Version:          &versions.VersionInfo{Version: 1},
   522  				})
   523  				select {
   524  				case resp := <-ch2:
   525  					if resp.Application != app {
   526  						t.Errorf("didn't receive correct application in for event %d, expected %q, got %q", i, app, resp.Application)
   527  					}
   528  				case <-time.After(1 * time.Second):
   529  					t.Fatalf("didn't receive event %d", i)
   530  				}
   531  			}
   532  			// Shutdown all consumers
   533  			cancel1()
   534  			cancel2()
   535  			cancel3()
   536  			// Unblock ch1
   537  			go func() {
   538  				for range ch1 {
   539  				}
   540  			}()
   541  			e1 := <-ech1
   542  			if e1 != nil {
   543  				t.Errorf("first subscription failed with unexpected error: %q", e1)
   544  			}
   545  			e2 := <-ech2
   546  			if e2 != nil {
   547  				t.Errorf("second subscription failed with unexpected error: %q", e2)
   548  			}
   549  			e3 := <-ech3
   550  			if e3 != testErr {
   551  				t.Errorf("third subscription failed with unexpected error: %q, exepcted: %q", e3, testErr)
   552  			}
   553  		})
   554  
   555  	}
   556  }
   557  
   558  func TestGetStatus(t *testing.T) {
   559  	t.Parallel()
   560  
   561  	tcs := []struct {
   562  		Name              string
   563  		ArgoEvents        []ArgoEvent
   564  		KuberpultEvents   []versions.KuberpultEvent
   565  		Request           *api.GetStatusRequest
   566  		DelayedArgoEvents []ArgoEvent
   567  
   568  		ExpectedResponse *api.GetStatusResponse
   569  	}{
   570  		{
   571  			Name:    "simple case",
   572  			Request: &api.GetStatusRequest{},
   573  			ExpectedResponse: &api.GetStatusResponse{
   574  				Status: api.RolloutStatus_ROLLOUT_STATUS_SUCCESFUL,
   575  			},
   576  		},
   577  		{
   578  			Name: "filters for environmentGroup",
   579  			ArgoEvents: []ArgoEvent{
   580  				{
   581  					Application:      "foo",
   582  					Environment:      "dev",
   583  					Version:          &versions.VersionInfo{Version: 2},
   584  					SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   585  					HealthStatusCode: health.HealthStatusHealthy,
   586  				},
   587  				{
   588  					Application:      "foo",
   589  					Environment:      "prd",
   590  					Version:          &versions.VersionInfo{Version: 1},
   591  					SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   592  					HealthStatusCode: health.HealthStatusHealthy,
   593  				},
   594  			},
   595  			KuberpultEvents: []versions.KuberpultEvent{
   596  				{
   597  					Application:      "foo",
   598  					Environment:      "dev",
   599  					Version:          &versions.VersionInfo{Version: 3},
   600  					EnvironmentGroup: "dev-group",
   601  				},
   602  				{
   603  					Application:      "foo",
   604  					Environment:      "prd",
   605  					Version:          &versions.VersionInfo{Version: 3},
   606  					EnvironmentGroup: "prd-group",
   607  				},
   608  			},
   609  			Request: &api.GetStatusRequest{
   610  				EnvironmentGroup: "dev-group",
   611  			},
   612  			ExpectedResponse: &api.GetStatusResponse{
   613  				Status: api.RolloutStatus_ROLLOUT_STATUS_PENDING,
   614  				Applications: []*api.GetStatusResponse_ApplicationStatus{
   615  					{
   616  						Environment:   "dev",
   617  						Application:   "foo",
   618  						RolloutStatus: api.RolloutStatus_ROLLOUT_STATUS_PENDING,
   619  					},
   620  				},
   621  			},
   622  		},
   623  		{
   624  			Name: "processes late health events",
   625  			ArgoEvents: []ArgoEvent{
   626  				{
   627  					Application:      "foo",
   628  					Environment:      "dev",
   629  					Version:          &versions.VersionInfo{Version: 2},
   630  					SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   631  					HealthStatusCode: health.HealthStatusHealthy,
   632  				},
   633  			},
   634  			KuberpultEvents: []versions.KuberpultEvent{
   635  				{
   636  					Application:      "foo",
   637  					Environment:      "dev",
   638  					Version:          &versions.VersionInfo{Version: 3},
   639  					EnvironmentGroup: "dev-group",
   640  				},
   641  			},
   642  			// This signals that the application is now healthy
   643  			DelayedArgoEvents: []ArgoEvent{
   644  				{
   645  					Application:      "foo",
   646  					Environment:      "dev",
   647  					Version:          &versions.VersionInfo{Version: 3},
   648  					SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   649  					HealthStatusCode: health.HealthStatusHealthy,
   650  				},
   651  			},
   652  			Request: &api.GetStatusRequest{
   653  				EnvironmentGroup: "dev-group",
   654  				WaitSeconds:      1,
   655  			},
   656  			ExpectedResponse: &api.GetStatusResponse{
   657  				Status:       api.RolloutStatus_ROLLOUT_STATUS_SUCCESFUL,
   658  				Applications: []*api.GetStatusResponse_ApplicationStatus{},
   659  			},
   660  		},
   661  		{
   662  			Name: "processes late error events",
   663  			ArgoEvents: []ArgoEvent{
   664  				{
   665  					Application:      "foo",
   666  					Environment:      "dev",
   667  					Version:          &versions.VersionInfo{Version: 2},
   668  					SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   669  					HealthStatusCode: health.HealthStatusHealthy,
   670  				},
   671  			},
   672  			KuberpultEvents: []versions.KuberpultEvent{
   673  				{
   674  					Application:      "foo",
   675  					Environment:      "dev",
   676  					Version:          &versions.VersionInfo{Version: 3},
   677  					EnvironmentGroup: "dev-group",
   678  				},
   679  			},
   680  			// This signals that the application is now broken
   681  			DelayedArgoEvents: []ArgoEvent{
   682  				{
   683  					Application:      "foo",
   684  					Environment:      "dev",
   685  					Version:          &versions.VersionInfo{Version: 3},
   686  					SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   687  					HealthStatusCode: health.HealthStatusDegraded,
   688  					OperationState: &v1alpha1.OperationState{
   689  						Phase: common.OperationFailed,
   690  					},
   691  				},
   692  			},
   693  			Request: &api.GetStatusRequest{
   694  				EnvironmentGroup: "dev-group",
   695  				WaitSeconds:      1,
   696  			},
   697  			ExpectedResponse: &api.GetStatusResponse{
   698  				Status: api.RolloutStatus_ROLLOUT_STATUS_ERROR,
   699  				Applications: []*api.GetStatusResponse_ApplicationStatus{
   700  					{
   701  						Environment:   "dev",
   702  						Application:   "foo",
   703  						RolloutStatus: api.RolloutStatus_ROLLOUT_STATUS_ERROR,
   704  					},
   705  				},
   706  			},
   707  		},
   708  		{
   709  			Name: "excludes succesful applications",
   710  			ArgoEvents: []ArgoEvent{
   711  				{
   712  					Application:      "foo",
   713  					Environment:      "dev",
   714  					Version:          &versions.VersionInfo{Version: 2},
   715  					SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   716  					HealthStatusCode: health.HealthStatusHealthy,
   717  				},
   718  				{
   719  					Application:      "bar",
   720  					Environment:      "dev",
   721  					Version:          &versions.VersionInfo{Version: 1},
   722  					SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   723  					HealthStatusCode: health.HealthStatusHealthy,
   724  				},
   725  			},
   726  			KuberpultEvents: []versions.KuberpultEvent{
   727  				{
   728  					Application:      "foo",
   729  					Environment:      "dev",
   730  					Version:          &versions.VersionInfo{Version: 3},
   731  					EnvironmentGroup: "dev-group",
   732  				},
   733  				{
   734  					Application:      "bar",
   735  					Environment:      "dev",
   736  					Version:          &versions.VersionInfo{Version: 1},
   737  					EnvironmentGroup: "dev-group",
   738  				},
   739  			},
   740  			Request: &api.GetStatusRequest{
   741  				EnvironmentGroup: "dev-group",
   742  			},
   743  			ExpectedResponse: &api.GetStatusResponse{
   744  				Status: api.RolloutStatus_ROLLOUT_STATUS_PENDING,
   745  				Applications: []*api.GetStatusResponse_ApplicationStatus{
   746  					{
   747  						Environment:   "dev",
   748  						Application:   "foo",
   749  						RolloutStatus: api.RolloutStatus_ROLLOUT_STATUS_PENDING,
   750  					},
   751  				},
   752  			},
   753  		},
   754  		{
   755  			Name: "filters for environmentGroup and team",
   756  			ArgoEvents: []ArgoEvent{
   757  				{
   758  					Application:      "foo",
   759  					Environment:      "dev",
   760  					Version:          &versions.VersionInfo{Version: 2},
   761  					SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   762  					HealthStatusCode: health.HealthStatusHealthy,
   763  				},
   764  				{
   765  					Application:      "foo",
   766  					Environment:      "prd",
   767  					Version:          &versions.VersionInfo{Version: 1},
   768  					SyncStatusCode:   v1alpha1.SyncStatusCodeSynced,
   769  					HealthStatusCode: health.HealthStatusHealthy,
   770  				},
   771  			},
   772  			KuberpultEvents: []versions.KuberpultEvent{
   773  				{
   774  					Application:      "foo",
   775  					Environment:      "dev",
   776  					Version:          &versions.VersionInfo{Version: 3},
   777  					EnvironmentGroup: "dev-group",
   778  					Team:             "a",
   779  				},
   780  				{
   781  					Application:      "foo",
   782  					Environment:      "bar",
   783  					Version:          &versions.VersionInfo{Version: 3},
   784  					EnvironmentGroup: "dev-group",
   785  					Team:             "b",
   786  				},
   787  				{
   788  					Application:      "foo",
   789  					Environment:      "prd",
   790  					Version:          &versions.VersionInfo{Version: 3},
   791  					EnvironmentGroup: "prd-group",
   792  				},
   793  			},
   794  			Request: &api.GetStatusRequest{
   795  				EnvironmentGroup: "dev-group",
   796  				Team:             "b",
   797  			},
   798  			ExpectedResponse: &api.GetStatusResponse{
   799  				Status: api.RolloutStatus_ROLLOUT_STATUS_UNKNOWN,
   800  				Applications: []*api.GetStatusResponse_ApplicationStatus{
   801  					{
   802  						Environment:   "bar",
   803  						Application:   "foo",
   804  						RolloutStatus: api.RolloutStatus_ROLLOUT_STATUS_UNKNOWN,
   805  					},
   806  				},
   807  			},
   808  		},
   809  	}
   810  
   811  	for _, tc := range tcs {
   812  		tc := tc
   813  		t.Run(tc.Name, func(t *testing.T) {
   814  			t.Parallel()
   815  			bc := New()
   816  			for _, s := range tc.ArgoEvents {
   817  				bc.ProcessArgoEvent(context.Background(), s)
   818  			}
   819  			for _, s := range tc.KuberpultEvents {
   820  				bc.ProcessKuberpultEvent(context.Background(), s)
   821  			}
   822  
   823  			bc.waiting = func() {
   824  				for _, s := range tc.DelayedArgoEvents {
   825  					bc.ProcessArgoEvent(context.Background(), s)
   826  				}
   827  			}
   828  
   829  			resp, err := bc.GetStatus(context.Background(), tc.Request)
   830  			if err != nil {
   831  				t.Errorf("didn't expect an error but got %q", err)
   832  			}
   833  			if d := cmp.Diff(tc.ExpectedResponse, resp, protocmp.Transform()); d != "" {
   834  				t.Errorf("response mismatch:\ndiff:%s", d)
   835  			}
   836  		})
   837  
   838  		// This runs all test-cases again but delays all argoevents.
   839  		// The effect is that all apps will start as "unknown" and then will eventually converge.
   840  		t.Run(tc.Name+" (delay all argo events)", func(t *testing.T) {
   841  			bc := New()
   842  			for _, s := range tc.KuberpultEvents {
   843  				bc.ProcessKuberpultEvent(context.Background(), s)
   844  			}
   845  			bc.waiting = func() {
   846  				for _, s := range tc.ArgoEvents {
   847  					bc.ProcessArgoEvent(context.Background(), s)
   848  				}
   849  				for _, s := range tc.DelayedArgoEvents {
   850  					bc.ProcessArgoEvent(context.Background(), s)
   851  				}
   852  				bc.DisconnectAll()
   853  			}
   854  			var req api.GetStatusRequest
   855  			proto.Merge(&req, tc.Request)
   856  			req.WaitSeconds = 1
   857  
   858  			resp, err := bc.GetStatus(context.Background(), &req)
   859  			if err != nil {
   860  				t.Errorf("didn't expect an error but got %q", err)
   861  			}
   862  			if d := cmp.Diff(tc.ExpectedResponse, resp, protocmp.Transform()); d != "" {
   863  				t.Errorf("response mismatch:\ndiff:%s", d)
   864  			}
   865  		})
   866  	}
   867  }