|
4 | 4 | "context" |
5 | 5 | "encoding/base64" |
6 | 6 | "encoding/json" |
| 7 | + "fmt" |
7 | 8 | "net/http" |
8 | 9 | "net/http/httptest" |
9 | 10 | "strings" |
@@ -209,6 +210,98 @@ func TestArgoResourceDiff_HappyPath(t *testing.T) { |
209 | 210 | } |
210 | 211 | } |
211 | 212 |
|
| 213 | +// serveArgoDiff runs the handler for one ConfigMap managed-set entry and |
| 214 | +// returns the raw body plus the decoded response. |
| 215 | +func serveArgoDiff(t *testing.T, item argoapi.ResourceDiff) (string, argoResourceDiffResponse) { |
| 216 | + t.Helper() |
| 217 | + seedInClusterArgoApp(t, "argocd", "guestbook") |
| 218 | + items := managedResourcesJSON(t, item) |
| 219 | + srv, _ := startFakeArgoServer(t, func(w http.ResponseWriter, r *http.Request) { |
| 220 | + _, _ = w.Write(items) |
| 221 | + }) |
| 222 | + connectArgo(t, srv.URL) |
| 223 | + |
| 224 | + req := newDiffRequest("argocd", "guestbook", "kind="+item.Kind+"&resourceNamespace="+item.Namespace+"&resourceName="+item.Name) |
| 225 | + w := httptest.NewRecorder() |
| 226 | + (&Server{}).handleArgoResourceDiff(w, req) |
| 227 | + if w.Code != http.StatusOK { |
| 228 | + t.Fatalf("status = %d, body = %s", w.Code, w.Body.String()) |
| 229 | + } |
| 230 | + var resp argoResourceDiffResponse |
| 231 | + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { |
| 232 | + t.Fatalf("decode: %v", err) |
| 233 | + } |
| 234 | + return w.Body.String(), resp |
| 235 | +} |
| 236 | + |
| 237 | +func configMapManifest(data map[string]any) map[string]any { |
| 238 | + return map[string]any{ |
| 239 | + "apiVersion": "v1", "kind": "ConfigMap", |
| 240 | + "metadata": map[string]any{"name": "settings", "namespace": "default"}, |
| 241 | + "data": data, |
| 242 | + } |
| 243 | +} |
| 244 | + |
| 245 | +// TestArgoResourceDiff_FieldEntriesNeverNull pins that an empty field list is |
| 246 | +// sent as [] (UI consumers read its length), for an in-sync resource and for a |
| 247 | +// resource missing on one side. |
| 248 | +func TestArgoResourceDiff_FieldEntriesNeverNull(t *testing.T) { |
| 249 | + same := configMapManifest(map[string]any{"mode": "steady"}) |
| 250 | + cases := map[string]argoapi.ResourceDiff{ |
| 251 | + "in sync": { |
| 252 | + Kind: "ConfigMap", Namespace: "default", Name: "settings", |
| 253 | + PredictedLiveState: jsonManifest(t, same), NormalizedLiveState: jsonManifest(t, same), |
| 254 | + }, |
| 255 | + "missing from cluster": { |
| 256 | + Kind: "ConfigMap", Namespace: "default", Name: "settings", |
| 257 | + PredictedLiveState: jsonManifest(t, same), |
| 258 | + }, |
| 259 | + "pending prune": { |
| 260 | + Kind: "ConfigMap", Namespace: "default", Name: "settings", |
| 261 | + NormalizedLiveState: jsonManifest(t, same), |
| 262 | + }, |
| 263 | + } |
| 264 | + for name, item := range cases { |
| 265 | + t.Run(name, func(t *testing.T) { |
| 266 | + body, resp := serveArgoDiff(t, item) |
| 267 | + if !strings.Contains(body, `"fieldEntries":[]`) { |
| 268 | + t.Errorf("fieldEntries must serialize as []:\n%s", body) |
| 269 | + } |
| 270 | + if len(resp.FieldEntries) != 0 || resp.FieldEntriesTruncated { |
| 271 | + t.Errorf("fieldEntries = %+v, truncated = %v; want none", resp.FieldEntries, resp.FieldEntriesTruncated) |
| 272 | + } |
| 273 | + }) |
| 274 | + } |
| 275 | +} |
| 276 | + |
| 277 | +// TestArgoResourceDiff_FieldEntriesTruncated pins that a diff cut at the entry |
| 278 | +// cap says so, instead of presenting the cap as the exact count. |
| 279 | +func TestArgoResourceDiff_FieldEntriesTruncated(t *testing.T) { |
| 280 | + desired, live := map[string]any{}, map[string]any{} |
| 281 | + for i := 0; i < 60; i++ { |
| 282 | + key := fmt.Sprintf("key%02d", i) |
| 283 | + desired[key] = "desired" |
| 284 | + live[key] = "live" |
| 285 | + } |
| 286 | + body, resp := serveArgoDiff(t, argoapi.ResourceDiff{ |
| 287 | + Kind: "ConfigMap", Namespace: "default", Name: "settings", |
| 288 | + PredictedLiveState: jsonManifest(t, configMapManifest(desired)), |
| 289 | + NormalizedLiveState: jsonManifest(t, configMapManifest(live)), |
| 290 | + }) |
| 291 | + if !resp.FieldEntriesTruncated { |
| 292 | + t.Errorf("fieldEntriesTruncated = false for 60 differing fields:\n%s", body) |
| 293 | + } |
| 294 | + |
| 295 | + _, small := serveArgoDiff(t, argoapi.ResourceDiff{ |
| 296 | + Kind: "ConfigMap", Namespace: "default", Name: "settings", |
| 297 | + PredictedLiveState: jsonManifest(t, configMapManifest(map[string]any{"a": "1"})), |
| 298 | + NormalizedLiveState: jsonManifest(t, configMapManifest(map[string]any{"a": "2"})), |
| 299 | + }) |
| 300 | + if small.FieldEntriesTruncated { |
| 301 | + t.Errorf("fieldEntriesTruncated = true for a one-field diff") |
| 302 | + } |
| 303 | +} |
| 304 | + |
212 | 305 | func TestArgoResourceDiff_SecretRedacted(t *testing.T) { |
213 | 306 | seedInClusterArgoApp(t, "argocd", "guestbook") |
214 | 307 | newPass := base64.StdEncoding.EncodeToString([]byte("BRAND-NEW-PASSWORD")) |
@@ -268,20 +361,27 @@ func TestArgoResourceDiff_SecretRedacted(t *testing.T) { |
268 | 361 | if !resp.Redacted { |
269 | 362 | t.Errorf("Secret diff must set redacted=true") |
270 | 363 | } |
271 | | - // password differs → changed on both sides; shared identical → unchanged; |
272 | | - // extra present only on live → changed. |
273 | | - if !strings.Contains(resp.Desired, "password: "+redactedChanged) { |
| 364 | + // password differs → a different marker per side, so it still reads as a |
| 365 | + // change; shared identical → unchanged; extra present only on live → live marker. |
| 366 | + if !strings.Contains(resp.Desired, "password: "+redactedDesired) { |
274 | 367 | t.Errorf("desired.password should be marked changed:\n%s", resp.Desired) |
275 | 368 | } |
276 | | - if !strings.Contains(resp.Live, "password: "+redactedChanged) { |
| 369 | + if !strings.Contains(resp.Live, "password: "+redactedLive) { |
277 | 370 | t.Errorf("live.password should be marked changed:\n%s", resp.Live) |
278 | 371 | } |
279 | 372 | if !strings.Contains(resp.Desired, "shared: "+redactedUnchanged) { |
280 | 373 | t.Errorf("desired.shared should be marked unchanged:\n%s", resp.Desired) |
281 | 374 | } |
282 | | - if !strings.Contains(resp.Live, "extra: "+redactedChanged) { |
| 375 | + if !strings.Contains(resp.Live, "extra: "+redactedLive) { |
283 | 376 | t.Errorf("live-only key should be marked changed:\n%s", resp.Live) |
284 | 377 | } |
| 378 | + paths := map[string]bool{} |
| 379 | + for _, e := range resp.FieldEntries { |
| 380 | + paths[e.Path] = true |
| 381 | + } |
| 382 | + if !paths["data.password"] || !paths["data.extra"] || paths["data.shared"] { |
| 383 | + t.Errorf("field entries = %+v; want data.password and data.extra, not data.shared", resp.FieldEntries) |
| 384 | + } |
285 | 385 | } |
286 | 386 |
|
287 | 387 | // TestArgoResourceDiff_SecretRedactionFailClosed pins the fail-closed paths: |
|
0 commit comments