diff --git a/api/database/boltdb/json.go b/api/database/boltdb/json.go index d2daaf0d2..d2f70dbe8 100644 --- a/api/database/boltdb/json.go +++ b/api/database/boltdb/json.go @@ -8,12 +8,29 @@ import ( // MarshalObject encodes an object to binary format func MarshalObject(object interface{}) ([]byte, error) { + // Special case for the VERSION bucket. Here we're not using json + if v, ok := object.(string); ok { + return []byte(v), nil + } + return json.Marshal(object) } // UnmarshalObject decodes an object from binary data func UnmarshalObject(data []byte, object interface{}) error { - return json.Unmarshal(data, object) + // Special case for the VERSION bucket. Here we're not using json + // So we need to return it as a string + err := json.Unmarshal(data, object) + if err != nil { + if s, ok := object.(*string); ok { + *s = string(data) + return nil + } + + return err + } + + return nil } // UnmarshalObjectWithJsoniter decodes an object from binary data diff --git a/api/database/boltdb/json_test.go b/api/database/boltdb/json_test.go new file mode 100644 index 000000000..d60e73d6e --- /dev/null +++ b/api/database/boltdb/json_test.go @@ -0,0 +1,122 @@ +package boltdb + +import ( + "fmt" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" +) + +const jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","Credentials":{"MPSUser":"","MPSPassword":"","MPSToken":""},"DomainConfiguration":{"CertFileText":"","CertPassword":"","DomainName":""},"WirelessConfiguration":null},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}` + +func Test_MarshalObject(t *testing.T) { + is := assert.New(t) + + uuid := uuid.Must(uuid.NewV4()) + + tests := []struct { + object interface{} + expected string + }{ + { + object: nil, + expected: `null`, + }, + { + object: true, + expected: `true`, + }, + { + object: false, + expected: `false`, + }, + { + object: 123, + expected: `123`, + }, + { + object: "456", + expected: "456", + }, + { + object: uuid, + expected: "\"" + uuid.String() + "\"", + }, + { + object: uuid.String(), + expected: uuid.String(), + }, + { + object: map[string]interface{}{"key": "value"}, + expected: `{"key":"value"}`, + }, + { + object: []bool{true, false}, + expected: `[true,false]`, + }, + { + object: []int{1, 2, 3}, + expected: `[1,2,3]`, + }, + { + object: []string{"1", "2", "3"}, + expected: `["1","2","3"]`, + }, + { + object: []map[string]interface{}{{"key1": "value1"}, {"key2": "value2"}}, + expected: `[{"key1":"value1"},{"key2":"value2"}]`, + }, + { + object: []interface{}{1, "2", false, map[string]interface{}{"key1": "value1"}}, + expected: `[1,"2",false,{"key1":"value1"}]`, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) { + data, err := MarshalObject(test.object) + is.NoError(err) + is.Equal(test.expected, string(data)) + }) + } +} + +func Test_UnMarshalObject(t *testing.T) { + is := assert.New(t) + + // Based on actual data entering and what we expect out of the function + + tests := []struct { + object []byte + expected string + }{ + { + object: []byte(""), + expected: "", + }, + { + object: []byte("35"), + expected: "35", + }, + { + // An unmarshalled byte string should return the same without error + object: []byte("9ca4a1dd-a439-4593-b386-a7dfdc2e9fc6"), + expected: "9ca4a1dd-a439-4593-b386-a7dfdc2e9fc6", + }, + { + // An unmarshalled json object string should return the same as a string without error also + object: []byte(jsonobject), + expected: jsonobject, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) { + var object string + err := UnmarshalObject(test.object, &object) + is.NoError(err) + is.Equal(test.expected, string(object)) + }) + } +}