From 7bc3b77fbe3089b3d21bfe16ca4e285fa338b344 Mon Sep 17 00:00:00 2001 From: Vincent Serpoul Date: Wed, 3 Feb 2021 22:37:22 +0800 Subject: [PATCH] add toml store --- go.mod | 5 + go.sum | 6 + stores/stores.go | 92 ++++++------- stores/toml/store.go | 264 ++++++++++++++++++++++++++++++++++++++ stores/toml/store_test.go | 234 +++++++++++++++++++++++++++++++++ 5 files changed, 555 insertions(+), 46 deletions(-) create mode 100644 stores/toml/store.go create mode 100644 stores/toml/store_test.go diff --git a/go.mod b/go.mod index 37784afcd..db5dd14ad 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/runc v0.1.1 // indirect github.com/ory/dockertest v3.3.4+incompatible + github.com/pelletier/go-toml v1.8.2-0.20210203134853-b4f0a950bf95 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.4.2 github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 // indirect @@ -48,6 +49,10 @@ require ( google.golang.org/protobuf v1.25.0 gopkg.in/ini.v1 v1.44.0 gopkg.in/urfave/cli.v1 v1.20.0 +<<<<<<< HEAD gopkg.in/yaml.v3 v3.0.0-20210107172259-749611fa9fcc gotest.tools v2.2.0+incompatible // indirect +======= + gotest.tools v2.2.0+incompatible +>>>>>>> cc2ee88b7 (add toml store) ) diff --git a/go.sum b/go.sum index 976e3519a..8b40a09e0 100644 --- a/go.sum +++ b/go.sum @@ -217,6 +217,12 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ github.com/ory/dockertest v3.3.4+incompatible h1:VrpM6Gqg7CrPm3bL4Wm1skO+zFWLbh7/Xb5kGEbJRh8= github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/pelletier/go-toml v1.8.2-0.20210129133109-c9a09d8695a8 h1:W2oirYjelH+CnTvFIwDQU+oxSNrrUt7/w7CRQ8yZokA= +github.com/pelletier/go-toml v1.8.2-0.20210129133109-c9a09d8695a8/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/pelletier/go-toml v1.8.2-0.20210203134853-b4f0a950bf95 h1:MAWl0xCdavY7sCLWqBORpL8ppvxvue1/ffgghMMVe5E= +github.com/pelletier/go-toml v1.8.2-0.20210203134853-b4f0a950bf95/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/stores/stores.go b/stores/stores.go index da8781ab2..0627c0e6c 100644 --- a/stores/stores.go +++ b/stores/stores.go @@ -29,7 +29,7 @@ type SopsFile struct { // in the SOPS file by checking for nil. This way we can show the user a // helpful error message indicating that the metadata wasn't found, instead // of showing a cryptic parsing error - Metadata *Metadata `yaml:"sops" json:"sops" ini:"sops"` + Metadata *Metadata `toml:"sops" yaml:"sops" json:"sops" ini:"sops"` } // Metadata is stored in SOPS encrypted files, and it contains the information necessary to decrypt the file. @@ -37,72 +37,72 @@ type SopsFile struct { // in order to allow the binary format to stay backwards compatible over time, but at the same time allow the internal // representation SOPS uses to change over time. type Metadata struct { - ShamirThreshold int `yaml:"shamir_threshold,omitempty" json:"shamir_threshold,omitempty"` - KeyGroups []keygroup `yaml:"key_groups,omitempty" json:"key_groups,omitempty"` - KMSKeys []kmskey `yaml:"kms" json:"kms"` - GCPKMSKeys []gcpkmskey `yaml:"gcp_kms" json:"gcp_kms"` - AzureKeyVaultKeys []azkvkey `yaml:"azure_kv" json:"azure_kv"` - VaultKeys []vaultkey `yaml:"hc_vault" json:"hc_vault"` - AgeKeys []agekey `yaml:"age" json:"age"` - LastModified string `yaml:"lastmodified" json:"lastmodified"` - MessageAuthenticationCode string `yaml:"mac" json:"mac"` - PGPKeys []pgpkey `yaml:"pgp" json:"pgp"` - UnencryptedSuffix string `yaml:"unencrypted_suffix,omitempty" json:"unencrypted_suffix,omitempty"` - EncryptedSuffix string `yaml:"encrypted_suffix,omitempty" json:"encrypted_suffix,omitempty"` - UnencryptedRegex string `yaml:"unencrypted_regex,omitempty" json:"unencrypted_regex,omitempty"` - EncryptedRegex string `yaml:"encrypted_regex,omitempty" json:"encrypted_regex,omitempty"` - Version string `yaml:"version" json:"version"` + ShamirThreshold int `toml:"shamir_threshold,omitempty" yaml:"shamir_threshold,omitempty" json:"shamir_threshold,omitempty"` + KeyGroups []keygroup `toml:"key_groups,omitempty" yaml:"key_groups,omitempty" json:"key_groups,omitempty"` + KMSKeys []kmskey `toml:"kms" yaml:"kms" json:"kms"` + GCPKMSKeys []gcpkmskey `toml:"gcp_kms" yaml:"gcp_kms" json:"gcp_kms"` + AzureKeyVaultKeys []azkvkey `toml:"azure_kv" yaml:"azure_kv" json:"azure_kv"` + VaultKeys []vaultkey `toml:"hc_vault" yaml:"hc_vault" json:"hc_vault"` + AgeKeys []agekey `toml:"age" yaml:"age" json:"age"` + LastModified string `toml:"lastmodified" yaml:"lastmodified" json:"lastmodified"` + MessageAuthenticationCode string `toml:"mac" yaml:"mac" json:"mac"` + PGPKeys []pgpkey `toml:"pgp" yaml:"pgp" json:"pgp"` + UnencryptedSuffix string `toml:"unencrypted_suffix,omitempty" yaml:"unencrypted_suffix,omitempty" json:"unencrypted_suffix,omitempty"` + EncryptedSuffix string `toml:"encrypted_suffix,omitempty" yaml:"encrypted_suffix,omitempty" json:"encrypted_suffix,omitempty"` + UnencryptedRegex string `toml:"unencrypted_regex,omitempty" yaml:"unencrypted_regex,omitempty" json:"unencrypted_regex,omitempty"` + EncryptedRegex string `toml:"encrypted_regex,omitempty" yaml:"encrypted_regex,omitempty" json:"encrypted_regex,omitempty"` + Version string `toml:"version" yaml:"version" json:"version"` } type keygroup struct { - PGPKeys []pgpkey `yaml:"pgp,omitempty" json:"pgp,omitempty"` - KMSKeys []kmskey `yaml:"kms,omitempty" json:"kms,omitempty"` - GCPKMSKeys []gcpkmskey `yaml:"gcp_kms,omitempty" json:"gcp_kms,omitempty"` - AzureKeyVaultKeys []azkvkey `yaml:"azure_kv,omitempty" json:"azure_kv,omitempty"` - VaultKeys []vaultkey `yaml:"hc_vault" json:"hc_vault"` - AgeKeys []agekey `yaml:"age" json:"age"` + PGPKeys []pgpkey `toml:"pgp,omitempty" yaml:"pgp,omitempty" json:"pgp,omitempty"` + KMSKeys []kmskey `toml:"kms,omitempty" yaml:"kms,omitempty" json:"kms,omitempty"` + GCPKMSKeys []gcpkmskey `toml:"gcp_kms,omitempty" yaml:"gcp_kms,omitempty" json:"gcp_kms,omitempty"` + AzureKeyVaultKeys []azkvkey `toml:"azure_kv,omitempty" yaml:"azure_kv,omitempty" json:"azure_kv,omitempty"` + VaultKeys []vaultkey `toml:"hc_vault" yaml:"hc_vault" json:"hc_vault"` + AgeKeys []agekey `toml:"age" yaml:"age" json:"age"` } type pgpkey struct { - CreatedAt string `yaml:"created_at" json:"created_at"` - EncryptedDataKey string `yaml:"enc" json:"enc"` - Fingerprint string `yaml:"fp" json:"fp"` + CreatedAt string `toml:"created_at" yaml:"created_at" json:"created_at"` + EncryptedDataKey string `toml:"enc" yaml:"enc" json:"enc"` + Fingerprint string `toml:"fp" yaml:"fp" json:"fp"` } type kmskey struct { - Arn string `yaml:"arn" json:"arn"` - Role string `yaml:"role,omitempty" json:"role,omitempty"` - Context map[string]*string `yaml:"context,omitempty" json:"context,omitempty"` - CreatedAt string `yaml:"created_at" json:"created_at"` - EncryptedDataKey string `yaml:"enc" json:"enc"` - AwsProfile string `yaml:"aws_profile" json:"aws_profile"` + Arn string `toml:"arn" yaml:"arn" json:"arn"` + Role string `toml:"role,omitempty" yaml:"role,omitempty" json:"role,omitempty"` + Context map[string]*string `toml:"context,omitempty" yaml:"context,omitempty" json:"context,omitempty"` + CreatedAt string `toml:"created_at" yaml:"created_at" json:"created_at"` + EncryptedDataKey string `toml:"enc" yaml:"enc" json:"enc"` + AwsProfile string `toml:"aws_profile" yaml:"aws_profile" json:"aws_profile"` } type gcpkmskey struct { - ResourceID string `yaml:"resource_id" json:"resource_id"` - CreatedAt string `yaml:"created_at" json:"created_at"` - EncryptedDataKey string `yaml:"enc" json:"enc"` + ResourceID string `toml:"resource_id" yaml:"resource_id" json:"resource_id"` + CreatedAt string `toml:"created_at" yaml:"created_at" json:"created_at"` + EncryptedDataKey string `toml:"enc" yaml:"enc" json:"enc"` } type vaultkey struct { - VaultAddress string `yaml:"vault_address" json:"vault_address"` - EnginePath string `yaml:"engine_path" json:"engine_path"` - KeyName string `yaml:"key_name" json:"key_name"` - CreatedAt string `yaml:"created_at" json:"created_at"` - EncryptedDataKey string `yaml:"enc" json:"enc"` + VaultAddress string `toml:"vault_address" yaml:"vault_address" json:"vault_address"` + EnginePath string `toml:"engine_path" yaml:"engine_path" json:"engine_path"` + KeyName string `toml:"key_name" yaml:"key_name" json:"key_name"` + CreatedAt string `toml:"created_at" yaml:"created_at" json:"created_at"` + EncryptedDataKey string `toml:"enc" yaml:"enc" json:"enc"` } type azkvkey struct { - VaultURL string `yaml:"vault_url" json:"vault_url"` - Name string `yaml:"name" json:"name"` - Version string `yaml:"version" json:"version"` - CreatedAt string `yaml:"created_at" json:"created_at"` - EncryptedDataKey string `yaml:"enc" json:"enc"` + VaultURL string `toml:"vault_url" yaml:"vault_url" json:"vault_url"` + Name string `toml:"name" yaml:"name" json:"name"` + Version string `toml:"version" yaml:"version" json:"version"` + CreatedAt string `toml:"created_at" yaml:"created_at" json:"created_at"` + EncryptedDataKey string `toml:"enc" yaml:"enc" json:"enc"` } type agekey struct { - Recipient string `yaml:"recipient" json:"recipient"` - EncryptedDataKey string `yaml:"enc" json:"enc"` + Recipient string `toml:"recipient" yaml:"recipient" json:"recipient"` + EncryptedDataKey string `toml:"enc" yaml:"enc" json:"enc"` } // MetadataFromInternal converts an internal SOPS metadata representation to a representation appropriate for storage diff --git a/stores/toml/store.go b/stores/toml/store.go new file mode 100644 index 000000000..78afbec0b --- /dev/null +++ b/stores/toml/store.go @@ -0,0 +1,264 @@ +package toml //import "go.mozilla.org/sops/v3/stores/toml" + +import ( + "errors" + "fmt" + "sort" + + "github.com/pelletier/go-toml" + "go.mozilla.org/sops/v3" + "go.mozilla.org/sops/v3/stores" +) + +// Store handles storage of TOML data. +type Store struct{} + +// positionKey is necessary to keep the order when we append in branches. +type positionKey struct { + position int + key string +} + +type byPosition []positionKey + +func (b byPosition) Less(i, j int) bool { return b[i].position < b[j].position } +func (b byPosition) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b byPosition) Len() int { return len(b) } + +var errUnexpectedValue = errors.New("unexpected value") + +func tomlTreeToTreeBranch(tr *toml.Tree) (sops.TreeBranch, error) { + treeItems := make(map[string]sops.TreeItem) + pks := []positionKey{} + + for k, v := range tr.Values() { + switch node := v.(type) { + case []*toml.Tree: + maxPosition := 0 + + var treeBranches []interface{} + + for _, item := range node { + if item.Position().Line > maxPosition { + maxPosition = item.Position().Line + } + + branch, errT := tomlTreeToTreeBranch(item) + if errT != nil { + return nil, fmt.Errorf("tomlTreeToTreeBranch: %w - %v, %s", errUnexpectedValue, v, k) + } + + treeBranches = append(treeBranches, branch) + } + + pks = append(pks, positionKey{position: maxPosition, key: k}) + treeItems[k] = sops.TreeItem{ + Key: k, + Value: treeBranches, + } + case *toml.Tree: + pks = append(pks, positionKey{position: node.Position().Line, key: k}) + + branch, errT := tomlTreeToTreeBranch(node) + if errT != nil { + return nil, fmt.Errorf("tomlTreeToTreeBranch: %w - %v, %s", errUnexpectedValue, v, k) + } + + treeItems[k] = sops.TreeItem{ + Key: k, + Value: branch, + } + case *toml.PubTOMLValue: + pks = append(pks, positionKey{position: node.Position().Line, key: k}) + treeItems[k] = sops.TreeItem{ + Key: k, + Value: node.Value(), + } + default: + return nil, fmt.Errorf("tomlTreeToTreeBranch: %w - %v, %s", errUnexpectedValue, v, k) + } + } + + sort.Sort(byPosition(pks)) + + var br sops.TreeBranch + for _, pk := range pks { + br = append(br, treeItems[pk.key]) + } + + return br, nil +} + +func treeBranchToTOMLTree(stree sops.TreeBranch) (*toml.Tree, error) { + ttree := &toml.PubTree{} + values := make(map[string]interface{}) + + for _, treeItem := range stree { + var errT error + + values[treeItem.Key.(string)], errT = treeItemValueToTOML(treeItem.Value) + if errT != nil { + return nil, errT + } + } + + ttree.SetValues(values) + + return ttree, nil +} + +func treeItemValueToTOML(treeItemValue interface{}) (interface{}, error) { + switch treeItemValueTyped := treeItemValue.(type) { + case sops.TreeBranch: + return treeBranchToTOMLTree(treeItemValueTyped) + + case []interface{}: + switch treeItemValueTyped[0].(type) { + case sops.TreeBranch: + var array []*toml.Tree + + for _, itm := range treeItemValueTyped { + tb := itm.(sops.TreeBranch) + + tr, errT := treeBranchToTOMLTree(tb) + if errT != nil { + return nil, errT + } + + array = append(array, tr) + } + + return array, nil + default: + val := &toml.PubTOMLValue{} + val.SetValue(treeItemValueTyped) + + return val, nil + } + + default: + val := &toml.PubTOMLValue{} + val.SetValue(treeItemValueTyped) + + return val, nil + } +} + +// LoadEncryptedFile loads the contents of an encrypted toml file onto a +// sops.Tree runtime object. +func (s *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) { + data, err := toml.LoadBytes(in) + if err != nil { + return sops.Tree{}, fmt.Errorf("error unmarshalling input toml: %w", err) + } + + // Because we don't know what fields the input file will have, we have to + // load the file in two steps. + // First, we load the file's metadata, the structure of which is known. + metadataHolder := stores.SopsFile{} + if err := data.Unmarshal(&metadataHolder); err != nil { + return sops.Tree{}, fmt.Errorf("error unmarshalling input toml: %w", err) + } + + if metadataHolder.Metadata == nil { + return sops.Tree{}, sops.MetadataNotFound + } + + metadata, err := metadataHolder.Metadata.ToInternal() + if err != nil { + return sops.Tree{}, fmt.Errorf("error unmarshalling input toml: %w", err) + } + + branch, errT := tomlTreeToTreeBranch(data) + if errT != nil { + return sops.Tree{}, fmt.Errorf("error transforming toml Tree: %w", err) + } + + return sops.Tree{ + Branches: sops.TreeBranches{branch}, + Metadata: metadata, + }, nil +} + +// LoadPlainFile loads the contents of a plaintext toml file onto a +// sops.Tree runtime object. +func (s *Store) LoadPlainFile(in []byte) (sops.TreeBranches, error) { + tomlTree, err := toml.LoadBytes(in) + if err != nil { + return nil, fmt.Errorf("error unmarshalling input toml: %w", err) + } + + branch, errT := tomlTreeToTreeBranch(tomlTree) + if errT != nil { + return nil, fmt.Errorf("error transforming toml Tree: %w", err) + } + + return sops.TreeBranches{branch}, nil +} + +var errTOMLUniqueDocument = errors.New("toml can only contain 1 document") + +// EmitEncryptedFile returns the encrypted bytes of the toml file corresponding to a +// sops.Tree runtime object. +func (s *Store) EmitEncryptedFile(in sops.Tree) ([]byte, error) { + if len(in.Branches) != 1 { + return nil, errTOMLUniqueDocument + } + + tomlTree, err := treeBranchToTOMLTree(in.Branches[0]) + if err != nil { + return nil, errTOMLUniqueDocument + } + + values := tomlTree.Values() + values["sops"] = stores.MetadataFromInternal(in.Metadata) + tomlTree.SetValues(values) + + return []byte(tomlTree.String()), nil +} + +// EmitPlainFile returns the plaintext bytes of the toml file corresponding to a +// sops.TreeBranches runtime object. +func (s *Store) EmitPlainFile(branches sops.TreeBranches) ([]byte, error) { + if len(branches) != 1 { + return nil, errTOMLUniqueDocument + } + + tomlTree, err := treeBranchToTOMLTree(branches[0]) + if err != nil { + return nil, fmt.Errorf("emit plain file: %w", err) + } + + return tomlTree.Marshal() +} + +// EmitValue returns bytes corresponding to a single encoded value +// in a generic interface{} object. +func (s *Store) EmitValue(v interface{}) ([]byte, error) { + switch v := v.(type) { + case sops.TreeBranch: + tomlTree, err := treeBranchToTOMLTree(v) + if err != nil { + return nil, fmt.Errorf("emit plain file: %w", err) + } + + return tomlTree.Marshal() + default: + str, err := toml.ValueStringRepresentation(v, "", "", toml.OrderPreserve, false) + if err != nil { + return nil, fmt.Errorf("emit plain file: %w", err) + } + return []byte(str), nil + } + +} + +// EmitExample returns the bytes corresponding to an example complex tree. +func (s *Store) EmitExample() []byte { + bytes, err := s.EmitPlainFile(stores.ExampleComplexTree.Branches) + if err != nil { + panic(err) + } + + return bytes +} diff --git a/stores/toml/store_test.go b/stores/toml/store_test.go new file mode 100644 index 000000000..8393f8af3 --- /dev/null +++ b/stores/toml/store_test.go @@ -0,0 +1,234 @@ +package toml + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "go.mozilla.org/sops/v3" +) + +func testPlain() []byte { + return []byte( + `# 0 comment +0 = 0 +1 = 1 # 1 comment + +[2] + 21 = [21.1, 21.2] # 21 comment + 22 = 22 + +[2.1] + 211 = 211 + +[[2.1.1]] + 2111 = 2111 + +[[2.1.1]] + 2112 = 2112 + +[[3]] + 31 = "thirty one" + +[[3]] + 32 = "thirty two" + +# 4 comment +[4] + # 41 comment + 41 = 41 +`) +} + +func testPlainNoComment() []byte { + return []byte( + `0 = 0 +1 = 1 + +[2] + 21 = [21.1, 21.2] + 22 = 22 + + [2.1] + 211 = 211 + + [[2.1.1]] + 2111 = 2111 + + [[2.1.1]] + 2112 = 2112 + +[[3]] + 31 = "thirty one" + +[[3]] + 32 = "thirty two" + +[4] + 41 = 41 +`) +} + +func testTreeBranches() sops.TreeBranches { + return sops.TreeBranches{ + sops.TreeBranch{ + // NOT IMPL on go-toml yet + // sops.TreeItem{ + // Key: sops.Comment{ + // Value: " 0 comment", + // }, + // Value: interface{}(nil), + // }, + sops.TreeItem{ + Key: "0", + Value: int64(0), + }, + sops.TreeItem{ + Key: "1", + Value: int64(1), + }, + // NOT IMPL on go-toml yet + // sops.TreeItem{ + // Key: sops.Comment{ + // Value: " 1 comment", + // }, + // Value: interface{}(nil), + // }, + sops.TreeItem{ + Key: "2", + Value: sops.TreeBranch{ + // NOT IMPL on go-toml yet + // sops.TreeItem{ + // Key: sops.Comment{ + // Value: " 21 comment", + // }, + // Value: interface{}(nil), + // }, + sops.TreeItem{ + Key: "21", + Value: []interface{}{ + 21.1, + 21.2, + }, + }, + sops.TreeItem{ + Key: "22", + Value: int64(22), + }, + sops.TreeItem{ + Key: "1", + Value: sops.TreeBranch{ + sops.TreeItem{ + Key: "211", + Value: int64(211), + }, + sops.TreeItem{ + Key: "1", + Value: []interface{}{ + sops.TreeBranch{ + sops.TreeItem{ + Key: "2111", + Value: int64(2111), + }, + }, + sops.TreeBranch{ + sops.TreeItem{ + Key: "2112", + Value: int64(2112), + }, + }, + }, + }, + }, + }, + }, + }, + sops.TreeItem{ + Key: "3", + Value: []interface{}{ + sops.TreeBranch{ + sops.TreeItem{ + Key: "31", + Value: "thirty one", + }, + }, + sops.TreeBranch{ + sops.TreeItem{ + Key: "32", + Value: "thirty two", + }, + // NOT IMPL on go-toml yet + // sops.TreeItem{ + // Key: sops.Comment{ + // Value: " 4 comment", + // }, + // Value: interface{}(nil), + // }, + }, + }, + }, + // NOT IMPL on go-toml yet + // sops.TreeItem{ + // Key: sops.Comment{ + // Value: " 41 comment", + // }, + // Value: interface{}(nil), + // }, + sops.TreeItem{ + Key: "4", + Value: sops.TreeBranch{ + sops.TreeItem{ + Key: "41", + Value: int64(41), + }, + }, + }, + }, + } +} + +func TestLoadPlainFile(t *testing.T) { + t.Parallel() + + actualBranches, err := (&Store{}).LoadPlainFile(testPlain()) + if err != nil { + t.Errorf("expected no error, got: %v", err) + + return + } + + expectedBranches := testTreeBranches() + + if !reflect.DeepEqual(expectedBranches, actualBranches) { + t.Errorf("expected\n%#v\ngot\n%#v", expectedBranches, actualBranches) + + return + } +} + +func TestEmitPlainFile(t *testing.T) { + t.Parallel() + + branches := testTreeBranches() + + bytes, err := (&Store{}).EmitPlainFile(branches) + if err != nil { + t.Errorf("expected no error, got: %v", err) + + return + } + + if !reflect.DeepEqual(testPlainNoComment(), bytes) { + t.Errorf("expected\n\n-%s-\n\ngot\n\n-%s-", testPlainNoComment(), bytes) + + return + } +} + +func TestEmitValueString(t *testing.T) { + t.Parallel() + + bytes, err := (&Store{}).EmitValue("hello") + assert.Nil(t, err) + assert.Equal(t, []byte("\"hello\""), bytes) +}