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 }