package workflows import ( "testing" portainer "github.com/portainer/portainer/api" gittypes "github.com/portainer/portainer/api/git/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestStackLastSyncDate(t *testing.T) { t.Parallel() t.Run("no deployment status", func(t *testing.T) { t.Parallel() assert.Equal(t, int64(0), StackLastSyncDate(portainer.Stack{})) }) t.Run("no active entry", func(t *testing.T) { t.Parallel() s := portainer.Stack{DeploymentStatus: []portainer.StackDeploymentStatus{ {Status: portainer.StackStatusDeploying, Time: 100}, }} assert.Equal(t, int64(0), StackLastSyncDate(s)) }) t.Run("last entry is active", func(t *testing.T) { t.Parallel() s := portainer.Stack{DeploymentStatus: []portainer.StackDeploymentStatus{ {Status: portainer.StackStatusDeploying, Time: 50}, {Status: portainer.StackStatusActive, Time: 100}, }} assert.Equal(t, int64(100), StackLastSyncDate(s)) }) t.Run("active followed by non-active returns the active time", func(t *testing.T) { t.Parallel() s := portainer.Stack{DeploymentStatus: []portainer.StackDeploymentStatus{ {Status: portainer.StackStatusActive, Time: 100}, {Status: portainer.StackStatusDeploying, Time: 200}, }} assert.Equal(t, int64(100), StackLastSyncDate(s)) }) } func TestEdgeStackLastSyncDate(t *testing.T) { t.Parallel() t.Run("empty statuses", func(t *testing.T) { t.Parallel() assert.Equal(t, int64(0), edgeStackLastSyncDate(nil)) }) t.Run("no healthy status for endpoint", func(t *testing.T) { t.Parallel() statuses := []portainer.EdgeStackStatusForEnv{ {EndpointID: 1, Status: []portainer.EdgeStackDeploymentStatus{ {Type: portainer.EdgeStackStatusDeploying, Time: 100}, }}, } assert.Equal(t, int64(0), edgeStackLastSyncDate(statuses)) }) t.Run("single endpoint with healthy status", func(t *testing.T) { t.Parallel() statuses := []portainer.EdgeStackStatusForEnv{ {EndpointID: 1, Status: []portainer.EdgeStackDeploymentStatus{ {Type: portainer.EdgeStackStatusRunning, Time: 200}, }}, } assert.Equal(t, int64(200), edgeStackLastSyncDate(statuses)) }) t.Run("returns minimum healthy time across endpoints", func(t *testing.T) { t.Parallel() statuses := []portainer.EdgeStackStatusForEnv{ {EndpointID: 1, Status: []portainer.EdgeStackDeploymentStatus{ {Type: portainer.EdgeStackStatusRunning, Time: 300}, }}, {EndpointID: 2, Status: []portainer.EdgeStackDeploymentStatus{ {Type: portainer.EdgeStackStatusRunning, Time: 100}, }}, } assert.Equal(t, int64(100), edgeStackLastSyncDate(statuses)) }) t.Run("one endpoint not yet synced returns 0", func(t *testing.T) { t.Parallel() statuses := []portainer.EdgeStackStatusForEnv{ {EndpointID: 1, Status: []portainer.EdgeStackDeploymentStatus{ {Type: portainer.EdgeStackStatusRunning, Time: 200}, }}, {EndpointID: 2, Status: []portainer.EdgeStackDeploymentStatus{ {Type: portainer.EdgeStackStatusDeploying, Time: 100}, }}, } assert.Equal(t, int64(0), edgeStackLastSyncDate(statuses)) }) } func TestEdgeStackTargetStatuses(t *testing.T) { t.Parallel() ep := func(id portainer.EndpointID, typ portainer.EdgeStackStatusType) portainer.EdgeStackStatusForEnv { return portainer.EdgeStackStatusForEnv{ EndpointID: id, Status: []portainer.EdgeStackDeploymentStatus{{Type: typ}}, } } t.Run("group with no endpoints is unknown", func(t *testing.T) { t.Parallel() result := edgeStackTargetStatuses( []portainer.EdgeGroupID{1}, nil, map[portainer.EdgeGroupID][]portainer.EndpointID{1: {}}, ) assert.Equal(t, StatusUnknown, result[portainer.EdgeGroupID(1)]) }) t.Run("group inherits highest-priority endpoint status", func(t *testing.T) { t.Parallel() result := edgeStackTargetStatuses( []portainer.EdgeGroupID{1}, []portainer.EdgeStackStatusForEnv{ ep(1, portainer.EdgeStackStatusRunning), ep(2, portainer.EdgeStackStatusDeploying), }, map[portainer.EdgeGroupID][]portainer.EndpointID{1: {1, 2}}, ) assert.Equal(t, StatusSyncing, result[portainer.EdgeGroupID(1)]) }) t.Run("multiple groups tracked separately", func(t *testing.T) { t.Parallel() result := edgeStackTargetStatuses( []portainer.EdgeGroupID{10, 20}, []portainer.EdgeStackStatusForEnv{ ep(1, portainer.EdgeStackStatusRunning), ep(2, portainer.EdgeStackStatusError), }, map[portainer.EdgeGroupID][]portainer.EndpointID{ 10: {1}, 20: {2}, }, ) assert.Equal(t, StatusHealthy, result[portainer.EdgeGroupID(10)]) assert.Equal(t, StatusError, result[portainer.EdgeGroupID(20)]) }) } func TestMapEdgeStackToWorkflow_DockerPlatform(t *testing.T) { t.Parallel() es := portainer.EdgeStack{ ID: 1, Name: "docker-edge", DeploymentType: portainer.EdgeStackDeploymentCompose, EdgeGroups: []portainer.EdgeGroupID{1}, CreationDate: 1587399600, } cfg := &gittypes.RepoConfig{URL: "https://github.com/x/repo"} w := MapEdgeStackToWorkflow(es, cfg, nil, map[portainer.EdgeGroupID][]portainer.EndpointID{1: {10}}, WorkflowPhaseStatus{Status: StatusHealthy}, WorkflowPhaseStatus{Status: StatusHealthy}) require.Equal(t, int(es.ID), w.ID) require.Equal(t, es.Name, w.Name) require.Equal(t, TypeEdgeStack, w.Type) require.Equal(t, DeploymentPlatformDockerStandalone, w.Platform) require.Equal(t, es.CreationDate, w.CreationDate) require.Equal(t, cfg, w.GitConfig) require.Equal(t, []portainer.EdgeGroupID{1}, w.Target.EdgeGroupIDs) } func TestMapEdgeStackToWorkflow_KubernetesPlatform(t *testing.T) { t.Parallel() es := portainer.EdgeStack{ ID: 2, Name: "kube-edge", DeploymentType: portainer.EdgeStackDeploymentKubernetes, EdgeGroups: []portainer.EdgeGroupID{1}, } w := MapEdgeStackToWorkflow(es, nil, nil, map[portainer.EdgeGroupID][]portainer.EndpointID{}, WorkflowPhaseStatus{Status: StatusUnknown}, WorkflowPhaseStatus{Status: StatusUnknown}) require.Equal(t, DeploymentPlatformKubernetes, w.Platform) } func TestMapEdgeStackToWorkflow_GroupStatusesAndResolvedEndpoints(t *testing.T) { t.Parallel() statuses := []portainer.EdgeStackStatusForEnv{ {EndpointID: 10, Status: []portainer.EdgeStackDeploymentStatus{{Type: portainer.EdgeStackStatusRunning}}}, {EndpointID: 20, Status: []portainer.EdgeStackDeploymentStatus{{Type: portainer.EdgeStackStatusError, Error: "boom"}}}, } groupEndpoints := map[portainer.EdgeGroupID][]portainer.EndpointID{ 1: {10}, 2: {20}, } es := portainer.EdgeStack{ ID: 3, Name: "multi-group", EdgeGroups: []portainer.EdgeGroupID{1, 2}, } w := MapEdgeStackToWorkflow(es, nil, statuses, groupEndpoints, WorkflowPhaseStatus{Status: StatusUnknown}, WorkflowPhaseStatus{Status: StatusUnknown}) require.Equal(t, StatusHealthy, w.Target.GroupStatus[1]) require.Equal(t, StatusError, w.Target.GroupStatus[2]) require.Len(t, w.Target.ResolvedEndpointIDs, 2) } func TestPlatformFromStackType(t *testing.T) { t.Parallel() require.Equal(t, DeploymentPlatformKubernetes, platformFromStackType(portainer.KubernetesStack)) require.Equal(t, DeploymentPlatformDockerSwarm, platformFromStackType(portainer.DockerSwarmStack)) require.Equal(t, DeploymentPlatformDockerStandalone, platformFromStackType(portainer.DockerComposeStack)) require.Equal(t, DeploymentPlatformDockerStandalone, platformFromStackType(portainer.StackType(99))) } func TestResolveEdgeGroupEndpoints_Empty(t *testing.T) { t.Parallel() result := resolveEdgeGroupEndpoints(nil, map[portainer.EdgeGroupID][]portainer.EndpointID{}) require.Empty(t, result) } func TestResolveEdgeGroupEndpoints_DeduplicatesAcrossGroups(t *testing.T) { t.Parallel() groupEndpoints := map[portainer.EdgeGroupID][]portainer.EndpointID{ 1: {10, 20}, 2: {20, 30}, } result := resolveEdgeGroupEndpoints([]portainer.EdgeGroupID{1, 2}, groupEndpoints) require.Len(t, result, 3) } func TestIsEdgeStackHealthyStatus(t *testing.T) { t.Parallel() healthyTypes := []portainer.EdgeStackStatusType{ portainer.EdgeStackStatusRunning, portainer.EdgeStackStatusRolledBack, portainer.EdgeStackStatusCompleted, portainer.EdgeStackStatusRemoved, portainer.EdgeStackStatusRemoteUpdateSuccess, } for _, typ := range healthyTypes { require.True(t, isEdgeStackHealthyStatus(typ)) } unhealthyTypes := []portainer.EdgeStackStatusType{ portainer.EdgeStackStatusError, portainer.EdgeStackStatusDeploying, portainer.EdgeStackStatusPending, } for _, typ := range unhealthyTypes { require.False(t, isEdgeStackHealthyStatus(typ)) } }