cli-utils/cmd/printers/json/formatter_test.go

405 lines
11 KiB
Go

// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package json
import (
"encoding/json"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/genericclioptions"
"sigs.k8s.io/cli-utils/pkg/apply/event"
"sigs.k8s.io/cli-utils/pkg/common"
pollevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
"sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/cli-utils/pkg/print/list"
)
func TestFormatter_FormatApplyEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.ApplyEvent
applyStats *list.ApplyStats
statusCollector list.Collector
expected []map[string]interface{}
}{
"resource created without dryrun": {
previewStrategy: common.DryRunNone,
event: event.ApplyEvent{
Operation: event.Created,
Type: event.ApplyEventResourceUpdate,
Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
},
expected: []map[string]interface{}{
{
"eventType": "resourceApplied",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "default",
"operation": "Created",
"timestamp": "",
"type": "apply",
},
},
},
"resource updated with client dryrun": {
previewStrategy: common.DryRunClient,
event: event.ApplyEvent{
Operation: event.Configured,
Type: event.ApplyEventResourceUpdate,
Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
},
expected: []map[string]interface{}{
{
"eventType": "resourceApplied",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "",
"operation": "Configured",
"timestamp": "",
"type": "apply",
},
},
},
"resource updated with server dryrun": {
previewStrategy: common.DryRunServer,
event: event.ApplyEvent{
Operation: event.Configured,
Type: event.ApplyEventResourceUpdate,
Identifier: createIdentifier("batch", "CronJob", "foo", "my-cron"),
},
expected: []map[string]interface{}{
{
"eventType": "resourceApplied",
"group": "batch",
"kind": "CronJob",
"name": "my-cron",
"namespace": "foo",
"operation": "Configured",
"timestamp": "",
"type": "apply",
},
},
},
"completed event": {
previewStrategy: common.DryRunNone,
event: event.ApplyEvent{
Type: event.ApplyEventCompleted,
},
applyStats: &list.ApplyStats{
ServersideApplied: 1,
},
statusCollector: &fakeCollector{
m: map[object.ObjMetadata]event.StatusEvent{
object.ObjMetadata{ //nolint:gofmt
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Namespace: "foo",
Name: "my-dep",
}: {
Resource: &pollevent.ResourceStatus{
Status: status.CurrentStatus,
Message: "Resource is Current",
},
},
},
},
expected: []map[string]interface{}{
{
"configuredCount": 0,
"count": 1,
"createdCount": 0,
"eventType": "completed",
"failedCount": 0,
"serverSideCount": 1,
"type": "apply",
"unchangedCount": 0,
"timestamp": "",
},
{
"eventType": "resourceStatus",
"group": "apps",
"kind": "Deployment",
"message": "Resource is Current",
"name": "my-dep",
"namespace": "foo",
"status": "Current",
"timestamp": "",
"type": "status",
},
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
formatter := NewFormatter(ioStreams, tc.previewStrategy)
err := formatter.FormatApplyEvent(tc.event, tc.applyStats, tc.statusCollector)
assert.NoError(t, err)
objects := strings.Split(strings.TrimSpace(out.String()), "\n")
if !assert.Equal(t, len(tc.expected), len(objects)) {
t.FailNow()
}
for i := range tc.expected {
assertOutput(t, tc.expected[i], objects[i])
}
})
}
}
func TestFormatter_FormatStatusEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.StatusEvent
statusCollector list.Collector
expected map[string]interface{}
}{
"resource update with Current status": {
previewStrategy: common.DryRunNone,
event: event.StatusEvent{
Type: event.StatusEventResourceUpdate,
Resource: &pollevent.ResourceStatus{
Identifier: object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Namespace: "foo",
Name: "bar",
},
Status: status.CurrentStatus,
Message: "Resource is Current",
},
},
expected: map[string]interface{}{
"eventType": "resourceStatus",
"group": "apps",
"kind": "Deployment",
"message": "Resource is Current",
"name": "bar",
"namespace": "foo",
"status": "Current",
"timestamp": "",
"type": "status",
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
formatter := NewFormatter(ioStreams, tc.previewStrategy)
err := formatter.FormatStatusEvent(tc.event, tc.statusCollector)
assert.NoError(t, err)
assertOutput(t, tc.expected, out.String())
})
}
}
func TestFormatter_FormatPruneEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.PruneEvent
pruneStats *list.PruneStats
expected map[string]interface{}
}{
"resource pruned without dryrun": {
previewStrategy: common.DryRunNone,
event: event.PruneEvent{
Operation: event.Pruned,
Type: event.PruneEventResourceUpdate,
Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourcePruned",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "default",
"operation": "Pruned",
"timestamp": "",
"type": "prune",
},
},
"resource skipped with client dryrun": {
previewStrategy: common.DryRunClient,
event: event.PruneEvent{
Operation: event.PruneSkipped,
Type: event.PruneEventResourceUpdate,
Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourcePruned",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "",
"operation": "PruneSkipped",
"timestamp": "",
"type": "prune",
},
},
"prune event with completed status": {
previewStrategy: common.DryRunNone,
event: event.PruneEvent{
Type: event.PruneEventCompleted,
},
pruneStats: &list.PruneStats{
Pruned: 1,
Skipped: 2,
},
expected: map[string]interface{}{
"eventType": "completed",
"skipped": 2,
"pruned": 1,
"timestamp": "",
"type": "prune",
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
formatter := NewFormatter(ioStreams, tc.previewStrategy)
err := formatter.FormatPruneEvent(tc.event, tc.pruneStats)
assert.NoError(t, err)
assertOutput(t, tc.expected, out.String())
})
}
}
func TestFormatter_FormatDeleteEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.DeleteEvent
deleteStats *list.DeleteStats
statusCollector list.Collector
expected map[string]interface{}
}{
"resource deleted without no dryrun": {
previewStrategy: common.DryRunNone,
event: event.DeleteEvent{
Operation: event.Deleted,
Type: event.DeleteEventResourceUpdate,
Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourceDeleted",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "default",
"operation": "Deleted",
"timestamp": "",
"type": "delete",
},
},
"resource skipped with client dryrun": {
previewStrategy: common.DryRunClient,
event: event.DeleteEvent{
Operation: event.DeleteSkipped,
Type: event.DeleteEventResourceUpdate,
Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourceDeleted",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "",
"operation": "DeleteSkipped",
"timestamp": "",
"type": "delete",
},
},
"delete event with completed status": {
previewStrategy: common.DryRunNone,
event: event.DeleteEvent{
Type: event.DeleteEventCompleted,
},
deleteStats: &list.DeleteStats{
Deleted: 1,
Skipped: 2,
},
expected: map[string]interface{}{
"deleted": 1,
"eventType": "completed",
"skipped": 2,
"timestamp": "",
"type": "delete",
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
formatter := NewFormatter(ioStreams, tc.previewStrategy)
err := formatter.FormatDeleteEvent(tc.event, tc.deleteStats)
assert.NoError(t, err)
assertOutput(t, tc.expected, out.String())
})
}
}
// nolint:unparam
func assertOutput(t *testing.T, expectedMap map[string]interface{}, actual string) bool {
var m map[string]interface{}
err := json.Unmarshal([]byte(actual), &m)
if !assert.NoError(t, err) {
return false
}
if _, found := expectedMap["timestamp"]; found {
if _, ok := m["timestamp"]; ok {
delete(expectedMap, "timestamp")
delete(m, "timestamp")
} else {
t.Error("expected to find key 'timestamp', but didn't")
return false
}
}
for key, val := range m {
if floatVal, ok := val.(float64); ok {
m[key] = int(floatVal)
}
}
return assert.Equal(t, expectedMap, m)
}
func createIdentifier(group, kind, namespace, name string) object.ObjMetadata {
return object.ObjMetadata{
Namespace: namespace,
Name: name,
GroupKind: schema.GroupKind{
Group: group,
Kind: kind,
},
}
}
type fakeCollector struct {
m map[object.ObjMetadata]event.StatusEvent
}
func (f *fakeCollector) LatestStatus() map[object.ObjMetadata]event.StatusEvent {
return f.m
}