diff --git a/audit/api/base64.go b/audit/api/base64.go new file mode 100644 index 0000000..1f6b287 --- /dev/null +++ b/audit/api/base64.go @@ -0,0 +1,76 @@ +package api + +import ( + "encoding/base64" + "encoding/json" + "errors" + "strings" +) + +var ErrBase64StringEmpty = errors.New("base64 string must not be empty") +var ErrOperationEmpty = errors.New("Operation must not be empty") +var ErrRoutableIdentifierNil = errors.New("routableIdentifier must not be nil") +var ErrUnsupportedBase64StringVersion = errors.New("unsupported base64 cloud event string version") + +type serializableEvent struct { + CloudEvent CloudEvent `json:"cloudEvent"` + Operation string `json:"operation"` + RoutableIdentifier RoutableIdentifier `json:"routableIdentifier"` +} + +func ToBase64( + cloudEvent *CloudEvent, + routableIdentifier *RoutableIdentifier, + operation string) (*string, error) { + + if cloudEvent == nil { + return nil, ErrCloudEventNil + } + + if routableIdentifier == nil { + return nil, ErrRoutableIdentifierNil + } + + if operation == "" { + return nil, ErrOperationEmpty + } + + event := serializableEvent{ + CloudEvent: *cloudEvent, + Operation: operation, + RoutableIdentifier: *routableIdentifier, + } + + serializedEvent, err := json.Marshal(event) + if err != nil { + return nil, err + } + + base64Str := base64.StdEncoding.EncodeToString(serializedEvent) + base64Str = base64Str + "v1" + return &base64Str, nil +} + +func FromBase64(base64Str string) (*CloudEvent, *RoutableIdentifier, *string, error) { + if base64Str == "" { + return nil, nil, nil, ErrBase64StringEmpty + } + + if !strings.HasSuffix(base64Str, "v1") { + return nil, nil, nil, ErrUnsupportedBase64StringVersion + } + base64Str = strings.TrimSuffix(base64Str, "v1") + + base64Bytes, err := base64.StdEncoding.DecodeString(base64Str) + if err != nil { + return nil, nil, nil, err + } + + event := serializableEvent{} + err = json.Unmarshal(base64Bytes, &event) + if err != nil { + return nil, nil, nil, err + } + + return &event.CloudEvent, &event.RoutableIdentifier, &event.Operation, nil +} diff --git a/audit/api/base64_test.go b/audit/api/base64_test.go new file mode 100644 index 0000000..d9de81c --- /dev/null +++ b/audit/api/base64_test.go @@ -0,0 +1,105 @@ +package api + +import ( + "encoding/base64" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_ToBase64(t *testing.T) { + + t.Run("cloud event nil", func(t *testing.T) { + var cloudEvent *CloudEvent = nil + routableIdentifier := RoutableSystemIdentifier + operation := "organization.create" + + base64str, err := ToBase64(cloudEvent, routableIdentifier, operation) + assert.ErrorIs(t, err, ErrCloudEventNil) + assert.Nil(t, base64str) + }) + + t.Run("routable identifier nil", func(t *testing.T) { + cloudEvent := &CloudEvent{} + var routableIdentifier *RoutableIdentifier = nil + operation := "organization.create" + + base64str, err := ToBase64(cloudEvent, routableIdentifier, operation) + assert.ErrorIs(t, err, ErrRoutableIdentifierNil) + assert.Nil(t, base64str) + }) + + t.Run("Operation empty", func(t *testing.T) { + cloudEvent := &CloudEvent{} + routableIdentifier := RoutableSystemIdentifier + operation := "" + + base64str, err := ToBase64(cloudEvent, routableIdentifier, operation) + assert.ErrorIs(t, err, ErrOperationEmpty) + assert.Nil(t, base64str) + }) + + t.Run("encoded event", func(t *testing.T) { + e := &CloudEvent{} + r := RoutableSystemIdentifier + o := "organization.create" + base64str, err := ToBase64(e, r, o) + assert.NoError(t, err) + + cloudEvent, routableIdentifier, operation, err := FromBase64(*base64str) + assert.NoError(t, err) + assert.Equal(t, e, cloudEvent) + assert.Equal(t, r, routableIdentifier) + assert.Equal(t, o, *operation) + }) +} + +func Test_FromBase64(t *testing.T) { + + t.Run("empty string", func(t *testing.T) { + cloudEvent, routableIdentifier, operation, err := FromBase64("") + assert.ErrorIs(t, err, ErrBase64StringEmpty) + assert.Nil(t, cloudEvent) + assert.Nil(t, routableIdentifier) + assert.Nil(t, operation) + }) + + t.Run("without version suffix", func(t *testing.T) { + cloudEvent, routableIdentifier, operation, err := FromBase64("ey") + assert.ErrorIs(t, err, ErrUnsupportedBase64StringVersion) + assert.Nil(t, cloudEvent) + assert.Nil(t, routableIdentifier) + assert.Nil(t, operation) + }) + + t.Run("no base64 string", func(t *testing.T) { + cloudEvent, routableIdentifier, operation, err := FromBase64("no base 64 v1") + assert.EqualError(t, err, "illegal base64 data at input byte 2") + assert.Nil(t, cloudEvent) + assert.Nil(t, routableIdentifier) + assert.Nil(t, operation) + }) + + t.Run("no json serialized event", func(t *testing.T) { + base64Str := base64.StdEncoding.EncodeToString([]byte("not expected")) + base64Str = base64Str + "v1" + cloudEvent, routableIdentifier, operation, err := FromBase64(base64Str) + assert.EqualError(t, err, "invalid character 'o' in literal null (expecting 'u')") + assert.Nil(t, cloudEvent) + assert.Nil(t, routableIdentifier) + assert.Nil(t, operation) + }) + + t.Run("decoded event", func(t *testing.T) { + e := &CloudEvent{} + r := RoutableSystemIdentifier + o := "organization.create" + base64str, err := ToBase64(e, r, o) + assert.NoError(t, err) + + cloudEvent, routableIdentifier, operation, err := FromBase64(*base64str) + assert.NoError(t, err) + assert.Equal(t, e, cloudEvent) + assert.Equal(t, r, routableIdentifier) + assert.Equal(t, o, *operation) + }) +}