Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions core/utils/utils.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package utils

import (
"encoding/base64"
"encoding/json"
)

// Ptr Returns the pointer to any type T
func Ptr[T any](v T) *T {
return &v
Expand All @@ -13,3 +18,50 @@ func Contains[T comparable](slice []T, element T) bool {
}
return false
}

// ConvertByteArraysToBase64 converts a struct to a map with byte arrays converted to base64 strings.
// This is useful for serialization formats like YAML where []byte fields should be displayed
// as base64 strings instead of byte arrays. Works with any struct containing []byte fields.
func ConvertByteArraysToBase64(obj interface{}) (map[string]interface{}, error) {
if obj == nil {
return nil, nil
}

// Marshal to JSON first to get the correct structure
jsonData, err := json.Marshal(obj)
if err != nil {
return nil, err
}

var result map[string]interface{}
if err := json.Unmarshal(jsonData, &result); err != nil {
return nil, err
}

// Convert byte arrays to base64 strings
convertByteArraysToBase64Recursive(result)

return result, nil
}

// convertByteArraysToBase64Recursive recursively converts []byte values to base64 strings
func convertByteArraysToBase64Recursive(data interface{}) {
switch v := data.(type) {
case map[string]interface{}:
for key, value := range v {
if byteArray, ok := value.([]byte); ok {
v[key] = base64.StdEncoding.EncodeToString(byteArray)
} else {
convertByteArraysToBase64Recursive(value)
}
}
case []interface{}:
for i, value := range v {
if byteArray, ok := value.([]byte); ok {
v[i] = base64.StdEncoding.EncodeToString(byteArray)
} else {
convertByteArraysToBase64Recursive(value)
}
}
}
}
164 changes: 164 additions & 0 deletions core/utils/utils_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package utils

import (
"encoding/base64"
"testing"
)

Expand Down Expand Up @@ -37,3 +38,166 @@ func TestContainsInt(t *testing.T) {
t.Fatalf("Should not be contained")
}
}

// Test struct for YAML conversion testing
type TestServer struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type TestServer struct {
type testServer struct {

doesn't need to be exposed I guess

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...or just move the struct definition into the TestConvertByteArraysToBase64 func. You're only using it there if I didn't mess something up here. See the example below for reference.

https://github.com/stackitcloud/stackit-cli/blob/a5438f4cac3a794cb95d04891a83252aa9ae1f1e/internal/pkg/utils/strings_test.go#L10-L13

Name string `json:"name"`
UserData *[]byte `json:"userData,omitempty"`
Data []byte `json:"data,omitempty"`
}

func TestConvertByteArraysToBase64(t *testing.T) {
tests := []struct {
name string
input interface{}
expectedFields map[string]interface{}
expectError bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have no test case in your table test where expectError is set to true. Then you don't have to have it here.

I would say either add some test cases which are expected to exit with an error or get rid of this input.

}{
{
name: "nil input",
input: nil,
expectError: false,
},
{
name: "normal case with byte arrays",
input: TestServer{
Name: "test-server",
UserData: func() *[]byte { b := []byte("hello world"); return &b }(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
UserData: func() *[]byte { b := []byte("hello world"); return &b }(),
UserData: Ptr([]byte("hello world"))

// Ptr Returns the pointer to any type T
func Ptr[T any](v T) *T {
return &v
}

Data: []byte("test data"),
},
expectedFields: map[string]interface{}{
"name": "test-server",
"userData": base64.StdEncoding.EncodeToString([]byte("hello world")),
"data": base64.StdEncoding.EncodeToString([]byte("test data")),
},
expectError: false,
},
{
name: "nil pointer case",
input: TestServer{
Name: "test-server",
UserData: nil,
Data: []byte("test"),
},
expectedFields: map[string]interface{}{
"name": "test-server",
"data": base64.StdEncoding.EncodeToString([]byte("test")),
},
expectError: false,
},
{
name: "empty byte array case",
input: TestServer{
Name: "test-server",
Data: []byte{},
},
expectedFields: map[string]interface{}{
"name": "test-server",
// Note: empty byte arrays are omitted from JSON output
},
expectError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ConvertByteArraysToBase64(tt.input)

if tt.expectError {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if tt.expectError {
if (err != nil) != tt.wantErr {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should handle all cases without needing 3 if conditions 😄

if err == nil {
t.Fatalf("Expected error but got none")
}
return
}

if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}

// Special case for nil input
if tt.input == nil {
if result != nil {
t.Fatalf("Expected nil result for nil input, got: %v", result)
}
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why stop the test case only because the input is nil?

}

// Check expected fields
for fieldName, expectedValue := range tt.expectedFields {
if actualValue, exists := result[fieldName]; !exists {
// For empty byte arrays, the field might be omitted from JSON
if fieldName == "data" && expectedValue == nil {
t.Logf("Empty byte array was omitted from JSON output, which is expected")
continue
}
t.Fatalf("Expected field %s to exist, but it doesn't", fieldName)
} else if actualValue != expectedValue {
t.Fatalf("Expected field %s to be %v, got %v", fieldName, expectedValue, actualValue)
}
}

// Check that no unexpected fields exist (except for omitted empty byte arrays)
for fieldName, actualValue := range result {
if _, expected := tt.expectedFields[fieldName]; !expected {
// Allow data field to exist even if not in expectedFields (for empty byte array case)
if fieldName == "data" && tt.name == "empty byte array case" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems weird to me checking for test case names here. Any reason?

continue
}
t.Fatalf("Unexpected field %s with value %v", fieldName, actualValue)
}
}
})
}
}

func TestConvertByteArraysToBase64Recursive(t *testing.T) {
tests := []struct {
name string
input interface{}
expected string // check if []byte was converted to base64 string
}{
{"nil", nil, ""},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"nil"? Accident or intended?

{"map with []byte", map[string]interface{}{"data": []byte("hello")}, "aGVsbG8="},
{"slice with []byte", []interface{}{[]byte("test")}, "dGVzdA=="},
{"nested map", map[string]interface{}{"level": map[string]interface{}{"data": []byte("nested")}}, "bmVzdGVk"},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please format the input for the table test properly.

{
  name: "nested map", 
  input: map[string]interface{}{
    "level": map[string]interface{}{
      "data": []byte("nested")
     },
  }, 
  expected: "bmVzdGVk"
},

Maybe it's just me, but I'm having a really hard time to get the goal of this PR in it's current state.

}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
convertByteArraysToBase64Recursive(tt.input)

if tt.expected == "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this skip of the further checks needed?

return
}

// check if base64 string in the result
found := findBase64String(tt.input)
if found != tt.expected {
t.Fatalf("Expected %s, got %s", tt.expected, found)
}
})
}
}

// helper to find base64 string in interface{}
func findBase64String(data interface{}) string {
switch v := data.(type) {
case map[string]interface{}:
for _, val := range v {
if str := findBase64String(val); str != "" {
return str
}
}
case []interface{}:
for _, val := range v {
if str := findBase64String(val); str != "" {
return str
}
}
case string:
if v != "" && v != "hello" && v != "test" && v != "nested" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hardcoded values? 🤔

return v
}
}
return ""
}
Loading