github.com/argoproj/argo-cd/v2@v2.10.9/server/badge/badge_test.go (about)

     1  package badge
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"image/color"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    13  	appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/fake"
    14  	"github.com/argoproj/argo-cd/v2/util/settings"
    15  
    16  	"github.com/argoproj/gitops-engine/pkg/health"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  	corev1 "k8s.io/api/core/v1"
    20  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    21  	"k8s.io/client-go/kubernetes/fake"
    22  )
    23  
    24  var (
    25  	argoCDSecret = corev1.Secret{
    26  		ObjectMeta: v1.ObjectMeta{Name: "argocd-secret", Namespace: "default"},
    27  		Data: map[string][]byte{
    28  			"admin.password":   []byte("test"),
    29  			"server.secretkey": []byte("test"),
    30  		},
    31  	}
    32  	argoCDCm = corev1.ConfigMap{
    33  		ObjectMeta: v1.ObjectMeta{
    34  			Name:      "argocd-cm",
    35  			Namespace: "default",
    36  			Labels: map[string]string{
    37  				"app.kubernetes.io/part-of": "argocd",
    38  			},
    39  		},
    40  		Data: map[string]string{
    41  			"statusbadge.enabled": "true",
    42  		},
    43  	}
    44  	testApp = v1alpha1.Application{
    45  		ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "default"},
    46  		Status: v1alpha1.ApplicationStatus{
    47  			Sync:   v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeSynced},
    48  			Health: v1alpha1.HealthStatus{Status: health.HealthStatusHealthy},
    49  			OperationState: &v1alpha1.OperationState{
    50  				SyncResult: &v1alpha1.SyncOperationResult{
    51  					Revision: "aa29b85",
    52  				},
    53  			},
    54  		},
    55  	}
    56  	testApp2 = v1alpha1.Application{
    57  		ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "argocd-test"},
    58  		Status: v1alpha1.ApplicationStatus{
    59  			Sync:   v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeSynced},
    60  			Health: v1alpha1.HealthStatus{Status: health.HealthStatusHealthy},
    61  			OperationState: &v1alpha1.OperationState{
    62  				SyncResult: &v1alpha1.SyncOperationResult{
    63  					Revision: "aa29b85",
    64  				},
    65  			},
    66  		},
    67  	}
    68  	testProject = v1alpha1.AppProject{
    69  		ObjectMeta: v1.ObjectMeta{Name: "test-project", Namespace: "default"},
    70  		Spec:       v1alpha1.AppProjectSpec{},
    71  	}
    72  )
    73  
    74  func TestHandlerFeatureIsEnabled(t *testing.T) {
    75  	settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), "default")
    76  	handler := NewHandler(appclientset.NewSimpleClientset(&testApp), settingsMgr, "default", []string{})
    77  	req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app", nil)
    78  	assert.NoError(t, err)
    79  
    80  	rr := httptest.NewRecorder()
    81  	handler.ServeHTTP(rr, req)
    82  
    83  	assert.Equal(t, "private, no-store", rr.Header().Get("Cache-Control"))
    84  	assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin"))
    85  
    86  	response := rr.Body.String()
    87  	assert.Equal(t, toRGBString(Green), leftRectColorPattern.FindStringSubmatch(response)[1])
    88  	assert.Equal(t, toRGBString(Green), rightRectColorPattern.FindStringSubmatch(response)[1])
    89  	assert.Equal(t, "Healthy", leftTextPattern.FindStringSubmatch(response)[1])
    90  	assert.Equal(t, "Synced", rightTextPattern.FindStringSubmatch(response)[1])
    91  	assert.NotContains(t, response, "(aa29b85)")
    92  }
    93  
    94  func TestHandlerFeatureProjectIsEnabled(t *testing.T) {
    95  	projectTests := []struct {
    96  		testApp     []*v1alpha1.Application
    97  		response    int
    98  		apiEndPoint string
    99  		namespace   string
   100  		health      string
   101  		status      string
   102  		healthColor color.RGBA
   103  		statusColor color.RGBA
   104  	}{
   105  		{createApplications([]string{"Healthy:Synced", "Healthy:Synced"}, []string{"default", "default"}, "test"),
   106  			http.StatusOK, "/api/badge?project=default", "test", "Healthy", "Synced", Green, Green},
   107  		{createApplications([]string{"Healthy:Synced", "Healthy:OutOfSync"}, []string{"test-project", "test-project"}, "default"),
   108  			http.StatusOK, "/api/badge?project=test-project", "default", "Healthy", "OutOfSync", Green, Orange},
   109  		{createApplications([]string{"Healthy:Synced", "Degraded:Synced"}, []string{"default", "default"}, "test"),
   110  			http.StatusOK, "/api/badge?project=default", "test", "Degraded", "Synced", Red, Green},
   111  		{createApplications([]string{"Healthy:Synced", "Degraded:OutOfSync"}, []string{"test-project", "test-project"}, "default"),
   112  			http.StatusOK, "/api/badge?project=test-project", "default", "Degraded", "OutOfSync", Red, Orange},
   113  		{createApplications([]string{"Healthy:Synced", "Healthy:Synced"}, []string{"test-project", "default"}, "test"),
   114  			http.StatusOK, "/api/badge?project=default&project=test-project", "test", "Healthy", "Synced", Green, Green},
   115  		{createApplications([]string{"Healthy:OutOfSync", "Healthy:Synced"}, []string{"test-project", "default"}, "default"),
   116  			http.StatusOK, "/api/badge?project=default&project=test-project", "default", "Healthy", "OutOfSync", Green, Orange},
   117  		{createApplications([]string{"Degraded:Synced", "Healthy:Synced"}, []string{"test-project", "default"}, "test"),
   118  			http.StatusOK, "/api/badge?project=default&project=test-project", "test", "Degraded", "Synced", Red, Green},
   119  		{createApplications([]string{"Degraded:OutOfSync", "Healthy:OutOfSync"}, []string{"test-project", "default"}, "default"),
   120  			http.StatusOK, "/api/badge?project=default&project=test-project", "default", "Degraded", "OutOfSync", Red, Orange},
   121  		{createApplications([]string{"Unknown:Unknown", "Unknown:Unknown"}, []string{"test-project", "default"}, "default"),
   122  			http.StatusOK, "/api/badge?project=", "default", "Unknown", "Unknown", Purple, Purple},
   123  		{createApplications([]string{"Unknown:Unknown", "Unknown:Unknown"}, []string{"test-project", "default"}, "default"),
   124  			http.StatusBadRequest, "/api/badge?project=test$project", "default", "Unknown", "Unknown", Purple, Purple},
   125  		{createApplications([]string{"Unknown:Unknown", "Unknown:Unknown"}, []string{"test-project", "default"}, "default"),
   126  			http.StatusOK, "/api/badge?project=unknown", "default", "Unknown", "Unknown", Purple, Purple},
   127  		{createApplications([]string{"Unknown:Unknown", "Unknown:Unknown"}, []string{"test-project", "default"}, "default"),
   128  			http.StatusBadRequest, "/api/badge?name=foo_bar", "default", "Unknown", "Unknown", Purple, Purple},
   129  		{createApplications([]string{"Unknown:Unknown", "Unknown:Unknown"}, []string{"test-project", "default"}, "default"),
   130  			http.StatusOK, "/api/badge?name=foobar", "default", "Not Found", "", Purple, Purple},
   131  	}
   132  	for _, tt := range projectTests {
   133  		argoCDCm.ObjectMeta.Namespace = tt.namespace
   134  		argoCDSecret.ObjectMeta.Namespace = tt.namespace
   135  		settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), tt.namespace)
   136  		handler := NewHandler(appclientset.NewSimpleClientset(&testProject, tt.testApp[0], tt.testApp[1]), settingsMgr, tt.namespace, []string{})
   137  		rr := httptest.NewRecorder()
   138  		req, err := http.NewRequest(http.MethodGet, tt.apiEndPoint, nil)
   139  		assert.NoError(t, err)
   140  		handler.ServeHTTP(rr, req)
   141  		require.Equal(t, tt.response, rr.Result().StatusCode)
   142  		if rr.Result().StatusCode != 400 {
   143  			assert.Equal(t, "private, no-store", rr.Header().Get("Cache-Control"))
   144  			assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin"))
   145  			response := rr.Body.String()
   146  			require.Greater(t, len(response), 2)
   147  			assert.Equal(t, toRGBString(tt.healthColor), leftRectColorPattern.FindStringSubmatch(response)[1])
   148  			assert.Equal(t, toRGBString(tt.statusColor), rightRectColorPattern.FindStringSubmatch(response)[1])
   149  			assert.Equal(t, tt.health, leftTextPattern.FindStringSubmatch(response)[1])
   150  			assert.Equal(t, tt.status, rightTextPattern.FindStringSubmatch(response)[1])
   151  		}
   152  	}
   153  }
   154  
   155  func TestHandlerNamespacesIsEnabled(t *testing.T) {
   156  	t.Run("Application in allowed namespace", func(t *testing.T) {
   157  		settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), "default")
   158  		handler := NewHandler(appclientset.NewSimpleClientset(&testApp2), settingsMgr, "default", []string{"argocd-test"})
   159  		req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app&namespace=argocd-test", nil)
   160  		assert.NoError(t, err)
   161  
   162  		rr := httptest.NewRecorder()
   163  		handler.ServeHTTP(rr, req)
   164  
   165  		assert.Equal(t, "private, no-store", rr.Header().Get("Cache-Control"))
   166  		assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin"))
   167  
   168  		response := rr.Body.String()
   169  		assert.Equal(t, toRGBString(Green), leftRectColorPattern.FindStringSubmatch(response)[1])
   170  		assert.Equal(t, toRGBString(Green), rightRectColorPattern.FindStringSubmatch(response)[1])
   171  		assert.Equal(t, "Healthy", leftTextPattern.FindStringSubmatch(response)[1])
   172  		assert.Equal(t, "Synced", rightTextPattern.FindStringSubmatch(response)[1])
   173  		assert.NotContains(t, response, "(aa29b85)")
   174  	})
   175  
   176  	t.Run("Application in disallowed namespace", func(t *testing.T) {
   177  		settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), "default")
   178  		handler := NewHandler(appclientset.NewSimpleClientset(&testApp2), settingsMgr, "default", []string{"argocd-test"})
   179  		req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app&namespace=kube-system", nil)
   180  		assert.NoError(t, err)
   181  
   182  		rr := httptest.NewRecorder()
   183  		handler.ServeHTTP(rr, req)
   184  
   185  		assert.Equal(t, http.StatusOK, rr.Result().StatusCode)
   186  		response := rr.Body.String()
   187  		assert.Equal(t, toRGBString(Purple), leftRectColorPattern.FindStringSubmatch(response)[1])
   188  		assert.Equal(t, toRGBString(Purple), rightRectColorPattern.FindStringSubmatch(response)[1])
   189  		assert.Equal(t, "Not Found", leftTextPattern.FindStringSubmatch(response)[1])
   190  		assert.Equal(t, "", rightTextPattern.FindStringSubmatch(response)[1])
   191  
   192  	})
   193  
   194  	t.Run("Request with illegal namespace", func(t *testing.T) {
   195  		settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), "default")
   196  		handler := NewHandler(appclientset.NewSimpleClientset(&testApp2), settingsMgr, "default", []string{"argocd-test"})
   197  		req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app&namespace=kube()system", nil)
   198  		assert.NoError(t, err)
   199  
   200  		rr := httptest.NewRecorder()
   201  		handler.ServeHTTP(rr, req)
   202  
   203  		assert.Equal(t, http.StatusBadRequest, rr.Result().StatusCode)
   204  	})
   205  }
   206  
   207  func createApplicationFeatureProjectIsEnabled(healthStatus health.HealthStatusCode, syncStatus v1alpha1.SyncStatusCode, appName, projectName, namespace string) *v1alpha1.Application {
   208  	return &v1alpha1.Application{
   209  		ObjectMeta: v1.ObjectMeta{Name: appName, Namespace: namespace},
   210  		Status: v1alpha1.ApplicationStatus{
   211  			Sync:   v1alpha1.SyncStatus{Status: syncStatus},
   212  			Health: v1alpha1.HealthStatus{Status: healthStatus},
   213  			OperationState: &v1alpha1.OperationState{
   214  				SyncResult: &v1alpha1.SyncOperationResult{},
   215  			},
   216  		},
   217  		Spec: v1alpha1.ApplicationSpec{
   218  			Project: projectName,
   219  		},
   220  	}
   221  }
   222  
   223  func createApplications(appCombo, projectName []string, namespace string) []*v1alpha1.Application {
   224  	apps := make([]*v1alpha1.Application, len(appCombo))
   225  	healthStatus := func(healthType string) health.HealthStatusCode {
   226  		switch healthType {
   227  		case "Healthy":
   228  			return health.HealthStatusHealthy
   229  		case "Degraded":
   230  			return health.HealthStatusDegraded
   231  		default:
   232  			return health.HealthStatusUnknown
   233  		}
   234  	}
   235  	syncStatus := func(syncType string) v1alpha1.SyncStatusCode {
   236  		switch syncType {
   237  		case "Synced":
   238  			return v1alpha1.SyncStatusCodeSynced
   239  		case "OutOfSync":
   240  			return v1alpha1.SyncStatusCodeOutOfSync
   241  		default:
   242  			return v1alpha1.SyncStatusCodeUnknown
   243  		}
   244  	}
   245  	for k, v := range appCombo {
   246  		a := strings.Split(v, ":")
   247  		healthApp := healthStatus(a[0])
   248  		syncApp := syncStatus(a[1])
   249  		appName := fmt.Sprintf("App %v", k)
   250  		apps[k] = createApplicationFeatureProjectIsEnabled(healthApp, syncApp, appName, projectName[k], namespace)
   251  	}
   252  	return apps
   253  }
   254  func TestHandlerFeatureIsEnabledRevisionIsEnabled(t *testing.T) {
   255  	settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), "default")
   256  	handler := NewHandler(appclientset.NewSimpleClientset(&testApp), settingsMgr, "default", []string{})
   257  	req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app&revision=true", nil)
   258  	assert.NoError(t, err)
   259  
   260  	rr := httptest.NewRecorder()
   261  	handler.ServeHTTP(rr, req)
   262  
   263  	assert.Equal(t, "private, no-store", rr.Header().Get("Cache-Control"))
   264  	assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin"))
   265  
   266  	response := rr.Body.String()
   267  	assert.Equal(t, toRGBString(Green), leftRectColorPattern.FindStringSubmatch(response)[1])
   268  	assert.Equal(t, toRGBString(Green), rightRectColorPattern.FindStringSubmatch(response)[1])
   269  	assert.Equal(t, "Healthy", leftTextPattern.FindStringSubmatch(response)[1])
   270  	assert.Equal(t, "Synced", rightTextPattern.FindStringSubmatch(response)[1])
   271  	assert.Contains(t, response, "(aa29b85)")
   272  }
   273  
   274  func TestHandlerRevisionIsEnabledNoOperationState(t *testing.T) {
   275  	app := testApp.DeepCopy()
   276  	app.Status.OperationState = nil
   277  
   278  	settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), "default")
   279  	handler := NewHandler(appclientset.NewSimpleClientset(app), settingsMgr, "default", []string{})
   280  	req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app&revision=true", nil)
   281  	assert.NoError(t, err)
   282  
   283  	rr := httptest.NewRecorder()
   284  	handler.ServeHTTP(rr, req)
   285  
   286  	assert.Equal(t, "private, no-store", rr.Header().Get("Cache-Control"))
   287  	assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin"))
   288  
   289  	response := rr.Body.String()
   290  	assert.Equal(t, toRGBString(Green), leftRectColorPattern.FindStringSubmatch(response)[1])
   291  	assert.Equal(t, toRGBString(Green), rightRectColorPattern.FindStringSubmatch(response)[1])
   292  	assert.Equal(t, "Healthy", leftTextPattern.FindStringSubmatch(response)[1])
   293  	assert.Equal(t, "Synced", rightTextPattern.FindStringSubmatch(response)[1])
   294  	assert.NotContains(t, response, "(aa29b85)")
   295  }
   296  
   297  func TestHandlerRevisionIsEnabledShortCommitSHA(t *testing.T) {
   298  	app := testApp.DeepCopy()
   299  	app.Status.OperationState.SyncResult.Revision = "abc"
   300  
   301  	settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), "default")
   302  	handler := NewHandler(appclientset.NewSimpleClientset(app), settingsMgr, "default", []string{})
   303  	req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app&revision=true", nil)
   304  	assert.NoError(t, err)
   305  
   306  	rr := httptest.NewRecorder()
   307  	handler.ServeHTTP(rr, req)
   308  
   309  	response := rr.Body.String()
   310  	assert.Contains(t, response, "(abc)")
   311  }
   312  
   313  func TestHandlerFeatureIsDisabled(t *testing.T) {
   314  
   315  	argoCDCmDisabled := argoCDCm.DeepCopy()
   316  	delete(argoCDCmDisabled.Data, "statusbadge.enabled")
   317  
   318  	settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(argoCDCmDisabled, &argoCDSecret), "default")
   319  	handler := NewHandler(appclientset.NewSimpleClientset(&testApp), settingsMgr, "default", []string{})
   320  	req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app", nil)
   321  	assert.NoError(t, err)
   322  
   323  	rr := httptest.NewRecorder()
   324  	handler.ServeHTTP(rr, req)
   325  
   326  	assert.Equal(t, "private, no-store", rr.Header().Get("Cache-Control"))
   327  	assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin"))
   328  
   329  	response := rr.Body.String()
   330  	assert.Equal(t, toRGBString(Purple), leftRectColorPattern.FindStringSubmatch(response)[1])
   331  	assert.Equal(t, toRGBString(Purple), rightRectColorPattern.FindStringSubmatch(response)[1])
   332  	assert.Equal(t, "Unknown", leftTextPattern.FindStringSubmatch(response)[1])
   333  	assert.Equal(t, "Unknown", rightTextPattern.FindStringSubmatch(response)[1])
   334  }