Files
portainer/api/http/handler/docker/containers/image_status_test.go
claude code agent f69eb3f9eb feat(automation): CE container image update detection endpoint + badge (#9, epic #3 M2)
Add native CE detection of "a newer image is available" for running
containers, surfaced as a read-only HTTP endpoint and a containers-list
badge/column. No applying of updates (M3/M4), no auto-heal (M1).

Backend:
- New CE handler GET /docker/{id}/containers/{containerId}/image_status
  backed by the existing zlib/CE digest engine
  (images.NewClientWithRegistry + ContainerImageStatus). Honors nodeName,
  authz, and routes registry calls through the credential store / SSRF
  AllowList. Engine failures degrade to a 200 {Status:"error"} so the UI
  stays graceful. Response shape: {Status, Message?}.

Frontend (CE-only, no isBE gating; the EE ImageStatus component is left
untouched):
- useContainerImageStatus TanStack Query hook (5min staleTime, no
  refetch-on-focus; backend caches 24h) calling the non-proxied endpoint.
- UpdateStatusBadge component (own assets, neutral on skipped/error).
- "Update available" column in the containers datatable; one cached,
  non-blocking query per visible row.

Tests: Go response-shape unit test; vitest for the badge (all statuses)
and the hook (url + nodeName query param via msw).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 08:59:54 +03:00

54 lines
1.4 KiB
Go

package containers
import (
"encoding/json"
"testing"
"github.com/portainer/portainer/api/docker/images"
)
// TestImageStatusResponse_JSONShape verifies the wire format of the image status
// response: the status string is mapped from the engine enum and the message is
// omitted when empty, matching what the CE frontend badge expects.
func TestImageStatusResponse_JSONShape(t *testing.T) {
tests := []struct {
name string
resp imageStatusResponse
expected string
}{
{
name: "outdated without message",
resp: imageStatusResponse{Status: string(images.Outdated)},
expected: `{"Status":"outdated"}`,
},
{
name: "updated without message",
resp: imageStatusResponse{Status: string(images.Updated)},
expected: `{"Status":"updated"}`,
},
{
name: "skipped without message",
resp: imageStatusResponse{Status: string(images.Skipped)},
expected: `{"Status":"skipped"}`,
},
{
name: "error carries a message",
resp: imageStatusResponse{Status: string(images.Error), Message: "registry unreachable"},
expected: `{"Status":"error","Message":"registry unreachable"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := json.Marshal(tt.resp)
if err != nil {
t.Fatalf("unexpected marshal error: %v", err)
}
if string(got) != tt.expected {
t.Errorf("unexpected JSON\n got: %s\nwant: %s", got, tt.expected)
}
})
}
}