test about half of what matters

This commit is contained in:
Roberto Hidalgo 2023-01-10 22:50:06 -06:00
parent 6f163b5e22
commit 632af1a2be
16 changed files with 989 additions and 107 deletions

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
# Copyright © 2021 Roberto Hidalgo <joao@un.rob.mx>
cd "$MILPA_REPO_ROOT" || @milpa.fail "could not cd into $MILPA_REPO_ROOT"
@milpa.log info "Running unit tests"
args=()
after_run=complete
if [[ "${MILPA_OPT_COVERAGE}" ]]; then
after_run=success
args=( -coverprofile=coverage.out --coverpkg=./...)
fi
gotestsum --format testname -- "$MILPA_ARG_SPEC" "${args[@]}" || exit 2
@milpa.log "$after_run" "Unit tests passed"
[[ ! "${MILPA_OPT_COVERAGE}" ]] && exit
@milpa.log info "Building coverage report"
go tool cover -html=coverage.out -o coverage.html || @milpa.fail "could not build reports"
@milpa.log complete "Coverage report ready at coverage.html"

View File

@ -0,0 +1,13 @@
summary: Runs unit tests
description: |
Runs unit tests using gotestsum
arguments:
- name: spec
default: ./...
description: the package to test
values:
dirs: "*"
options:
coverage:
type: bool
description: if provided, will output coverage reports

5
bad-test.yaml Normal file
View File

@ -0,0 +1,5 @@
_config: !!joao
name: some:test
vault: bad-example
int: -:a\

105
cmd/fetch_test.go Normal file
View File

@ -0,0 +1,105 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
// SPDX-License-Identifier: Apache-2.0
package cmd_test
import (
"bytes"
"strings"
"testing"
. "git.rob.mx/nidito/joao/cmd"
"git.rob.mx/nidito/joao/internal/op-client/mock"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func TestFetch(t *testing.T) {
mockOPConnect(t)
f := testConfig.Fields
s := testConfig.Sections
defer func() { testConfig.Fields = f; testConfig.Sections = s }()
testConfig.Sections = append(testConfig.Sections,
&onepassword.ItemSection{ID: "o", Label: "o"},
&onepassword.ItemSection{ID: "e-fez-tambem", Label: "e-fez-tambem"},
)
testConfig.Fields = append(testConfig.Fields,
&onepassword.ItemField{
ID: "o.ganso.gosto",
Section: &onepassword.ItemSection{ID: "o", Label: "o"},
Type: "STRING",
Label: "ganso.gosto",
Value: "da dupla",
},
&onepassword.ItemField{
ID: "e-fez-tambem.0",
Section: &onepassword.ItemSection{ID: "e-fez-tambem", Label: "e-fez-tambem"},
Type: "STRING",
Label: "0",
Value: "quém!",
},
&onepassword.ItemField{
ID: "e-fez-tambem.1",
Section: &onepassword.ItemSection{ID: "e-fez-tambem", Label: "e-fez-tambem"},
Type: "STRING",
Label: "1",
Value: "quém!",
},
&onepassword.ItemField{
ID: "e-fez-tambem.2",
Section: &onepassword.ItemSection{ID: "e-fez-tambem", Label: "e-fez-tambem"},
Type: "STRING",
Label: "2",
Value: "quém!",
})
mock.Update(testConfig)
root := fromProjectRoot()
out := bytes.Buffer{}
Fetch.SetBindings()
cmd := &cobra.Command{}
cmd.Flags().Bool("dry-run", true, "")
cmd.SetOut(&out)
cmd.SetErr(&out)
Fetch.Cobra = cmd
logrus.SetLevel(logrus.DebugLevel)
err := Fetch.Run(cmd, []string{root + "/test.yaml"})
if err != nil {
t.Fatalf("could not get: %s", err)
}
expected := `_config: !!joao
name: some:test
vault: example
# not sorted on purpose
int: 1 # line
# foot
string: pato
bool: false
secret: !!secret very secret
nested:
string: quem
int: 1
secret: !!secret very secret
bool: true
list:
- 1
- 2
- 3
list:
- one
- two
- three
o:
ganso:
gosto: da dupla
e-fez-tambem:
- quém!
- quém!
- quém!`
if got := out.String(); strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
}
}

View File

@ -77,6 +77,7 @@ looks at the filesystem or remotely, using 1password (over the CLI if available,
}
if query == "" || query == "." {
var bytes []byte
switch format {
case "yaml", "raw", "diff-yaml":
modes := []config.OutputMode{}
@ -86,21 +87,17 @@ looks at the filesystem or remotely, using 1password (over the CLI if available,
if format == "diff-yaml" {
modes = append(modes, config.OutputModeNoComments, config.OutputModeSorted)
}
bytes, err := cfg.AsYAML(modes...)
if err != nil {
return err
}
_, err = cmd.Cobra.OutOrStdout().Write(bytes)
return err
bytes, err = cfg.AsYAML(modes...)
case "json", "op":
bytes, err := cfg.AsJSON(redacted, format == "op")
if err != nil {
return err
}
_, err = cmd.Cobra.OutOrStdout().Write(bytes)
bytes, err = cfg.AsJSON(redacted, format == "op")
default:
return fmt.Errorf("unknown format %s", format)
}
if err != nil {
return err
}
return fmt.Errorf("unknown format %s", format)
_, err = cmd.Cobra.OutOrStdout().Write(bytes)
return err
}
parts := strings.Split(query, ".")

View File

@ -11,9 +11,166 @@ import (
"testing"
. "git.rob.mx/nidito/joao/cmd"
opclient "git.rob.mx/nidito/joao/internal/op-client"
"git.rob.mx/nidito/joao/internal/op-client/mock"
"github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var testConfig = &onepassword.Item{
Title: "some:test",
Vault: onepassword.ItemVault{ID: "example"},
Category: "PASSWORD",
Sections: []*onepassword.ItemSection{
{ID: "~annotations", Label: "~annotations"},
// {ID: "nested", Label: "nested"},
{ID: "list", Label: "list"},
},
Fields: []*onepassword.ItemField{
{
ID: "password",
Type: "CONCEALED",
Purpose: "PASSWORD",
Label: "password",
Value: "56615e9be5f0ce5f97d5b446faaa1d39f95a13a1ea8326ae933c3d29eb29735c",
},
{
ID: "notesPlain",
Type: "STRING",
Purpose: "NOTES",
Label: "notesPlain",
Value: "flushed by joao",
},
{
ID: "~annotations.int",
Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"},
Type: "STRING",
Label: "int",
Value: "int",
},
{
ID: "int",
Type: "STRING",
Label: "int",
Value: "1",
},
{
ID: "string",
Type: "STRING",
Label: "string",
Value: "pato",
},
{
ID: "~annotations.bool",
Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"},
Type: "STRING",
Label: "bool",
Value: "bool",
},
{
ID: "bool",
Type: "STRING",
Label: "bool",
Value: "false",
},
{
ID: "~annotations.secret",
Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"},
Type: "STRING",
Label: "secret",
Value: "secret",
},
{
ID: "secret",
Type: "CONCEALED",
Label: "secret",
Value: "very secret",
},
{
ID: "nested.string",
Section: &onepassword.ItemSection{ID: "nested", Label: "nested"},
Type: "STRING",
Label: "string",
Value: "quem",
},
{
ID: "~annotations.nested.int",
Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"},
Type: "STRING",
Label: "nested.int",
Value: "int",
},
{
ID: "nested.int",
Section: &onepassword.ItemSection{ID: "nested", Label: "nested"},
Type: "STRING",
Label: "int",
Value: "1",
},
{
ID: "~annotations.nested.secret",
Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"},
Type: "STRING",
Label: "nested.secret",
Value: "secret",
},
{
ID: "nested.secret",
Section: &onepassword.ItemSection{ID: "nested", Label: "nested"},
Type: "CONCEALED",
Label: "secret",
Value: "very secret",
},
{
ID: "~annotations.nested.bool",
Section: &onepassword.ItemSection{ID: "~annotations", Label: "~annotations"},
Type: "STRING",
Label: "nested.bool",
Value: "bool",
},
{
ID: "nested.bool",
Section: &onepassword.ItemSection{ID: "nested", Label: "nested"},
Type: "STRING",
Label: "bool",
Value: "true",
},
{
ID: "list.0",
Section: &onepassword.ItemSection{ID: "list", Label: "list"},
Type: "STRING",
Label: "0",
Value: "one",
},
{
ID: "list.1",
Section: &onepassword.ItemSection{ID: "list", Label: "list"},
Type: "STRING",
Label: "1",
Value: "two",
},
{
ID: "list.2",
Section: &onepassword.ItemSection{ID: "list", Label: "list"},
Type: "STRING",
Label: "2",
Value: "three",
},
},
}
func mockOPConnect(t *testing.T) {
t.Helper()
opclient.ConnectClientFactory = func(host, token, userAgent string) connect.Client {
return &mock.Client{}
}
client := opclient.NewConnect("", "")
opclient.Use(client)
mock.Add(testConfig)
}
func fromProjectRoot() string {
_, filename, _, _ := runtime.Caller(0)
dir := path.Join(path.Dir(filename), "../")
@ -24,29 +181,42 @@ func fromProjectRoot() string {
return wd
}
func TestGetRedacted(t *testing.T) {
func TestGetBadYAML(t *testing.T) {
root := fromProjectRoot()
out := bytes.Buffer{}
Get.SetBindings()
out := bytes.Buffer{}
cmd := &cobra.Command{}
cmd.Flags().Bool("redacted", true, "")
cmd.SetOut(&out)
cmd.SetErr(&out)
Get.Cobra = cmd
err := Get.Run(cmd, []string{root + "/test.yaml", ".", "--redacted"})
if err != nil {
t.Fatalf("could not get: %s", err)
err := Get.Run(cmd, []string{root + "/bad-test.yaml", "."})
if err == nil {
t.Fatalf("Did not throw on bad path: %s", out.String())
}
expected, err := os.ReadFile(root + "/test.yaml")
if err != nil {
t.Fatalf("could not read file: %s", err)
wantedPrefix := "could not parse file"
wantedSuffix := "/bad-test.yaml as yaml: line 4: mapping values are not allowed in this context"
if got := err.Error(); !(strings.HasPrefix(got, wantedPrefix) && strings.HasSuffix(got, wantedSuffix)) {
t.Fatalf("Failed with bad error, wanted %s /some-path%s, got %s", wantedPrefix, wantedSuffix, got)
}
}
got := out.String()
if strings.TrimSpace(got) != strings.ReplaceAll(strings.TrimSpace(string(expected)), " very secret", "") {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
func TestGetBadPath(t *testing.T) {
root := fromProjectRoot()
Get.SetBindings()
out := bytes.Buffer{}
cmd := &cobra.Command{}
cmd.SetOut(&out)
cmd.SetErr(&out)
Get.Cobra = cmd
err := Get.Run(cmd, []string{root + "/does-not-exist.yaml", "."})
if err == nil {
t.Fatalf("Did not throw on bad path: %s", out.String())
}
wantedPrefix := "could not read file"
wantedSuffix := "/does-not-exist.yaml"
if got := err.Error(); !(strings.HasPrefix(got, wantedPrefix) && strings.HasSuffix(got, wantedSuffix)) {
t.Fatalf("Failed with bad error, wanted %s /some-path%s, got %s", wantedPrefix, wantedSuffix, got)
}
}
@ -70,8 +240,32 @@ func TestGetNormal(t *testing.T) {
t.Fatalf("could not read file: %s", err)
}
got := out.String()
if strings.TrimSpace(got) != strings.TrimSpace(string(expected)) {
if got := out.String(); strings.TrimSpace(got) != strings.TrimSpace(string(expected)) {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
}
}
func TestGetRedacted(t *testing.T) {
root := fromProjectRoot()
out := bytes.Buffer{}
Get.SetBindings()
cmd := &cobra.Command{}
cmd.Flags().Bool("redacted", true, "")
cmd.SetOut(&out)
cmd.SetErr(&out)
Get.Cobra = cmd
err := Get.Run(cmd, []string{root + "/test.yaml", ".", "--redacted"})
if err != nil {
t.Fatalf("could not get: %s", err)
}
expected, err := os.ReadFile(root + "/test.yaml")
if err != nil {
t.Fatalf("could not read file: %s", err)
}
if got := out.String(); strings.TrimSpace(got) != strings.ReplaceAll(strings.TrimSpace(string(expected)), " very secret", "") {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
}
}
@ -82,6 +276,7 @@ func TestGetPath(t *testing.T) {
Get.SetBindings()
cmd := &cobra.Command{}
cmd.Flags().Bool("redacted", false, "")
cmd.Flags().StringP("output", "o", "yaml", "")
cmd.SetOut(&out)
cmd.SetErr(&out)
Get.Cobra = cmd
@ -92,8 +287,29 @@ func TestGetPath(t *testing.T) {
}
expected := "very secret"
got := out.String()
if strings.TrimSpace(got) != strings.TrimSpace(expected) {
if got := out.String(); strings.TrimSpace(got) != strings.TrimSpace(expected) {
t.Fatalf("did not get expected scalar output:\nwanted: %s\ngot: %s", expected, got)
}
out = bytes.Buffer{}
cmd.SetOut(&out)
cmd.SetErr(&out)
err = Get.Run(cmd, []string{root + "/test.yaml", "nested", "--output", "diff-yaml"})
if err != nil {
t.Fatalf("could not get: %s", err)
}
expected = `bool: true
int: 1
list:
- 1
- 2
- 3
secret: very secret
string: quem`
if got := out.String(); strings.TrimSpace(got) != strings.TrimSpace(expected) {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
}
}
@ -116,11 +332,14 @@ func TestGetPathCollection(t *testing.T) {
expected := `bool: true
int: 1
list:
- 1
- 2
- 3
secret: very secret
string: quem`
got := out.String()
if strings.TrimSpace(got) != expected {
if got := out.String(); strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
}
}
@ -153,13 +372,16 @@ list:
nested:
bool: true
int: 1
list:
- 1
- 2
- 3
secret: !!secret very secret
string: quem
secret: !!secret very secret
string: "pato"`
string: pato`
got := out.String()
if strings.TrimSpace(got) != expected {
if got := out.String(); strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
}
}
@ -180,10 +402,9 @@ func TestGetJSON(t *testing.T) {
t.Fatalf("could not get: %s", err)
}
expected := `{"bool":false,"int":1,"list":["one","two","three"],"nested":{"bool":true,"int":1,"secret":"very secret","string":"quem"},"secret":"very secret","string":"pato"}`
expected := `{"bool":false,"int":1,"list":["one","two","three"],"nested":{"bool":true,"int":1,"list":[1,2,3],"secret":"very secret","string":"quem"},"secret":"very secret","string":"pato"}`
got := out.String()
if strings.TrimSpace(got) != expected {
if got := out.String(); strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
}
}
@ -204,10 +425,9 @@ func TestGetJSONPathScalar(t *testing.T) {
t.Fatalf("could not get: %s", err)
}
expected := `very secret`
expected := `very secret` // nolint: ifshort
got := out.String()
if strings.TrimSpace(got) != expected {
if got := out.String(); strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
}
}
@ -228,10 +448,9 @@ func TestGetJSONPathCollection(t *testing.T) {
t.Fatalf("could not get: %s", err)
}
expected := `{"bool":true,"int":1,"secret":"very secret","string":"quem"}`
expected := `{"bool":true,"int":1,"list":[1,2,3],"secret":"very secret","string":"quem"}`
got := out.String()
if strings.TrimSpace(got) != expected {
if got := out.String(); strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
}
}
@ -252,10 +471,9 @@ func TestGetJSONRedacted(t *testing.T) {
t.Fatalf("could not get: %s", err)
}
expected := `{"bool":false,"int":1,"list":["one","two","three"],"nested":{"bool":true,"int":1,"secret":"","string":"quem"},"secret":"","string":"pato"}`
expected := `{"bool":false,"int":1,"list":["one","two","three"],"nested":{"bool":true,"int":1,"list":[1,2,3],"secret":"","string":"quem"},"secret":"","string":"pato"}`
got := out.String()
if strings.TrimSpace(got) != expected {
if got := out.String(); strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
}
}
@ -275,10 +493,47 @@ func TestGetJSONOP(t *testing.T) {
t.Fatalf("could not get: %s", err)
}
expected := `{"id":"","title":"some:test","vault":{"id":"example"},"category":"PASSWORD","sections":[{"id":"~annotations","label":"~annotations"},{"id":"nested","label":"nested"}],"fields":[{"id":"password","type":"CONCEALED","purpose":"PASSWORD","label":"password","value":"56615e9be5f0ce5f97d5b446faaa1d39f95a13a1ea8326ae933c3d29eb29735c"},{"id":"notesPlain","type":"STRING","purpose":"NOTES","label":"notesPlain","value":"flushed by joao"},{"id":"~annotations.int","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"int","value":"int"},{"id":"int","type":"STRING","label":"int","value":"1"},{"id":"string","type":"STRING","label":"string","value":"pato"},{"id":"~annotations.bool","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"bool","value":"bool"},{"id":"bool","type":"STRING","label":"bool","value":"false"},{"id":"~annotations.secret","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"secret","value":"secret"},{"id":"secret","type":"CONCEALED","label":"secret","value":"very secret"},{"id":"nested.string","section":{"id":"nested"},"type":"STRING","label":"string","value":"quem"},{"id":"~annotations.nested.int","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.int","value":"int"},{"id":"nested.int","section":{"id":"nested"},"type":"STRING","label":"int","value":"1"},{"id":"~annotations.nested.secret","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.secret","value":"secret"},{"id":"nested.secret","section":{"id":"nested"},"type":"CONCEALED","label":"secret","value":"very secret"},{"id":"~annotations.nested.bool","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.bool","value":"bool"},{"id":"nested.bool","section":{"id":"nested"},"type":"STRING","label":"bool","value":"true"},{"id":"list.0","section":{"id":"list"},"type":"STRING","label":"0","value":"one"},{"id":"list.1","section":{"id":"list"},"type":"STRING","label":"1","value":"two"},{"id":"list.2","section":{"id":"list"},"type":"STRING","label":"2","value":"three"}],"createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z"}`
expected := `{"id":"","title":"some:test","vault":{"id":"example"},"category":"PASSWORD","sections":[{"id":"~annotations","label":"~annotations"},{"id":"nested","label":"nested"},{"id":"list","label":"list"}],"fields":[{"id":"password","type":"CONCEALED","purpose":"PASSWORD","label":"password","value":"cedbdf86fb15cf1237569e9b3188372d623aea9d6a707401aca656645590227c"},{"id":"notesPlain","type":"STRING","purpose":"NOTES","label":"notesPlain","value":"flushed by joao"},{"id":"~annotations.int","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"int","value":"int"},{"id":"int","type":"STRING","label":"int","value":"1"},{"id":"string","type":"STRING","label":"string","value":"pato"},{"id":"~annotations.bool","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"bool","value":"bool"},{"id":"bool","type":"STRING","label":"bool","value":"false"},{"id":"~annotations.secret","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"secret","value":"secret"},{"id":"secret","type":"CONCEALED","label":"secret","value":"very secret"},{"id":"nested.string","section":{"id":"nested"},"type":"STRING","label":"string","value":"quem"},{"id":"~annotations.nested.int","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.int","value":"int"},{"id":"nested.int","section":{"id":"nested"},"type":"STRING","label":"int","value":"1"},{"id":"~annotations.nested.secret","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.secret","value":"secret"},{"id":"nested.secret","section":{"id":"nested"},"type":"CONCEALED","label":"secret","value":"very secret"},{"id":"~annotations.nested.bool","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.bool","value":"bool"},{"id":"nested.bool","section":{"id":"nested"},"type":"STRING","label":"bool","value":"true"},{"id":"~annotations.nested.list.0","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.list.0","value":"int"},{"id":"nested.list.0","section":{"id":"nested"},"type":"STRING","label":"list.0","value":"1"},{"id":"~annotations.nested.list.1","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.list.1","value":"int"},{"id":"nested.list.1","section":{"id":"nested"},"type":"STRING","label":"list.1","value":"2"},{"id":"~annotations.nested.list.2","section":{"id":"~annotations","label":"~annotations"},"type":"STRING","label":"nested.list.2","value":"int"},{"id":"nested.list.2","section":{"id":"nested"},"type":"STRING","label":"list.2","value":"3"},{"id":"list.0","section":{"id":"list"},"type":"STRING","label":"0","value":"one"},{"id":"list.1","section":{"id":"list"},"type":"STRING","label":"1","value":"two"},{"id":"list.2","section":{"id":"list"},"type":"STRING","label":"2","value":"three"}],"createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z"}`
got := out.String()
if strings.TrimSpace(got) != expected {
if got := out.String(); strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
}
}
func TestGetRemote(t *testing.T) {
mockOPConnect(t)
root := fromProjectRoot()
out := bytes.Buffer{}
Get.SetBindings()
cmd := &cobra.Command{}
cmd.Flags().Bool("redacted", false, "")
cmd.Flags().Bool("remote", true, "")
cmd.Flags().StringP("output", "o", "diff-yaml", "")
cmd.SetOut(&out)
cmd.SetErr(&out)
Get.Cobra = cmd
logrus.SetLevel(logrus.DebugLevel)
err := Get.Run(cmd, []string{root + "/test.yaml", ".", "--output", "diff-yaml", "--remote"})
if err != nil {
t.Fatalf("could not get: %s", err)
}
expected := `bool: false
int: 1
list:
- one
- two
- three
nested:
bool: true
int: 1
secret: !!secret very secret
string: quem
secret: !!secret very secret
string: pato`
if got := out.String(); strings.TrimSpace(got) != expected {
t.Fatalf("did not get expected output:\nwanted: %s\ngot: %s", expected, got)
}
}

View File

@ -5,6 +5,7 @@ package cmd
import (
"fmt"
"io/fs"
"io/ioutil"
"os"
"strings"
@ -89,7 +90,7 @@ Will read values from stdin (or ﹅--from﹅ a file) and store it at the ﹅PATH
}
if delete && input != "/dev/stdin" {
logrus.Warn("Ignoring --file while deleting")
logrus.Warn("Ignoring --input while deleting")
}
cfg, err = config.Load(path, false)
@ -104,7 +105,12 @@ Will read values from stdin (or ﹅--from﹅ a file) and store it at the ﹅PATH
return err
}
} else {
valueBytes, err := os.ReadFile(input)
var valueBytes []byte
if input == "/dev/stdin" {
valueBytes, err = ioutil.ReadAll(cmd.Cobra.InOrStdin())
} else {
valueBytes, err = os.ReadFile(input)
}
if err != nil {
return err
}

457
cmd/set_test.go Normal file
View File

@ -0,0 +1,457 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
// SPDX-License-Identifier: Apache-2.0
package cmd_test
import (
"bytes"
"fmt"
"io/fs"
"io/ioutil"
"os"
"strings"
"testing"
. "git.rob.mx/nidito/joao/cmd"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func tempTestYaml(root, name string, data []byte) (string, func(), error) {
path := fmt.Sprintf("%s/test-%s.yaml", root, name)
if err := ioutil.WriteFile(path, data, fs.FileMode(0644)); err != nil {
return path, nil, fmt.Errorf("could not create test file")
}
return path, func() { os.Remove(path) }, nil
}
func TestSet(t *testing.T) {
root := fromProjectRoot()
Set.SetBindings()
out := bytes.Buffer{}
cmd := &cobra.Command{}
cmd.SetOut(&out)
cmd.SetErr(&out)
stdin := bytes.Buffer{}
stdin.Write([]byte("pato\nganso\nmarreco\n"))
cmd.SetIn(&stdin)
Set.Cobra = cmd
cmd.Flags().Bool("secret", false, "")
cmd.Flags().Bool("delete", false, "")
cmd.Flags().Bool("json", false, "")
cmd.Flags().Bool("flush", false, "")
original, err := ioutil.ReadFile(root + "/test.yaml")
if err != nil {
t.Fatalf("could not read file")
}
path, cleanup, err := tempTestYaml(root, "set-plain", original)
if err != nil {
t.Fatal(err)
}
defer cleanup()
err = Set.Run(cmd, []string{path, "string"})
if err != nil {
t.Fatalf("Threw on good set: %s", err)
}
changed, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("could not read file")
}
if string(changed) == string(original) {
t.Fatal("Did not change file")
}
if !strings.Contains(string(changed), `
string: |-
pato
ganso
marreco`) {
t.Fatalf("Did not contain expected new string, got:\n%s", changed)
}
}
func TestSetSecret(t *testing.T) {
root := fromProjectRoot()
Set.SetBindings()
out := bytes.Buffer{}
cmd := &cobra.Command{}
cmd.SetOut(&out)
cmd.SetErr(&out)
stdin := bytes.Buffer{}
stdin.Write([]byte("new secret\n"))
cmd.SetIn(&stdin)
Set.Cobra = cmd
cmd.Flags().Bool("secret", true, "")
cmd.Flags().Bool("delete", false, "")
cmd.Flags().Bool("json", false, "")
cmd.Flags().Bool("flush", false, "")
original, err := ioutil.ReadFile(root + "/test.yaml")
if err != nil {
t.Fatalf("could not read file")
}
path, cleanup, err := tempTestYaml(root, "set-plain", original)
if err != nil {
t.Fatal(err)
}
defer cleanup()
err = Set.Run(cmd, []string{path, "secret", "--secret"})
if err != nil {
t.Fatalf("Threw on good set: %s", err)
}
changed, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("could not read file")
}
if string(changed) == string(original) {
t.Fatal("Did not change file")
}
if !strings.Contains(string(changed), "\nsecret: !!secret new secret\n") {
t.Fatalf("Did not contain expected new string, got:\n%s", changed)
}
}
func TestSetFromFile(t *testing.T) {
root := fromProjectRoot()
Set.SetBindings()
out := bytes.Buffer{}
cmd := &cobra.Command{}
cmd.SetOut(&out)
cmd.SetErr(&out)
Set.Cobra = cmd
cmd.Flags().Bool("secret", false, "")
cmd.Flags().Bool("delete", false, "")
cmd.Flags().Bool("json", false, "")
cmd.Flags().Bool("flush", false, "")
original, err := ioutil.ReadFile(root + "/test.yaml")
if err != nil {
t.Fatalf("could not read file")
}
dataPath, dataCleanup, err := tempTestYaml(root, "set-from-file-data", []byte("ganso"))
if err != nil {
t.Fatal(err)
}
defer dataCleanup()
cmd.Flags().StringP("input", "i", dataPath, "")
path, cleanup, err := tempTestYaml(root, "set-from-file", original)
if err != nil {
t.Fatal(err)
}
defer cleanup()
err = Set.Run(cmd, []string{path, "string", "--input", dataPath})
if err != nil {
t.Fatalf("Threw on good set: %s", err)
}
changed, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("could not read file")
}
if string(changed) == string(original) {
t.Fatal("Did not change file")
}
if !strings.Contains(string(changed), "\nstring: ganso\n") {
t.Fatalf("Did not contain expected new string, got:\n%s", changed)
}
}
func TestSetNew(t *testing.T) {
root := fromProjectRoot()
Set.SetBindings()
out := bytes.Buffer{}
cmd := &cobra.Command{}
cmd.SetOut(&out)
cmd.SetErr(&out)
stdin := bytes.Buffer{}
stdin.Write([]byte("pato\nganso\nmarreco\ncisne\n"))
cmd.SetIn(&stdin)
Set.Cobra = cmd
cmd.Flags().Bool("secret", false, "")
cmd.Flags().Bool("delete", false, "")
cmd.Flags().Bool("json", false, "")
cmd.Flags().Bool("flush", false, "")
cmd.Flags().StringP("input", "i", "/dev/stdin", "")
original, err := ioutil.ReadFile(root + "/test.yaml")
if err != nil {
t.Fatalf("could not read file")
}
path, cleanup, err := tempTestYaml(root, "set-new-key", original)
if err != nil {
t.Fatal(err)
}
defer cleanup()
err = Set.Run(cmd, []string{path, "quarteto"})
if err != nil {
t.Fatalf("Threw on good new set: %s", err)
}
changed, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("could not read file")
}
if string(changed) == string(original) {
t.Fatal("Did not change file")
}
if !strings.Contains(string(changed), `
quarteto: |-
pato
ganso
marreco
cisne`) {
t.Fatalf("Did not contain expected new string, got:\n%s", changed)
}
}
func TestSetNested(t *testing.T) {
root := fromProjectRoot()
Set.SetBindings()
out := bytes.Buffer{}
cmd := &cobra.Command{}
cmd.SetOut(&out)
cmd.SetErr(&out)
stdin := bytes.Buffer{}
stdin.Write([]byte("tico"))
cmd.SetIn(&stdin)
Set.Cobra = cmd
cmd.Flags().Bool("secret", false, "")
cmd.Flags().Bool("delete", false, "")
cmd.Flags().Bool("json", false, "")
cmd.Flags().Bool("flush", false, "")
cmd.Flags().StringP("input", "i", "/dev/stdin", "")
original, err := ioutil.ReadFile(root + "/test.yaml")
if err != nil {
t.Fatalf("could not read file")
}
path, cleanup, err := tempTestYaml(root, "set-nested-key", original)
if err != nil {
t.Fatal(err)
}
defer cleanup()
err = Set.Run(cmd, []string{path, "nested.tico"})
if err != nil {
t.Fatalf("Threw on good nested set: %s", err)
}
changed, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("could not read file")
}
if string(changed) == string(original) {
t.Fatal("Did not change file")
}
if !strings.Contains(string(changed), `
tico: tico
`) {
t.Fatalf("Did not contain expected new string, got:\n%s", changed)
}
}
func TestSetJSON(t *testing.T) {
root := fromProjectRoot()
Set.SetBindings()
out := bytes.Buffer{}
cmd := &cobra.Command{}
cmd.SetOut(&out)
cmd.SetErr(&out)
stdin := bytes.Buffer{}
stdin.Write([]byte(`{"foram": "ensaiar", "para": "começar"}`))
cmd.SetIn(&stdin)
Set.Cobra = cmd
cmd.Flags().Bool("secret", false, "")
cmd.Flags().Bool("delete", false, "")
cmd.Flags().Bool("json", true, "")
cmd.Flags().Bool("flush", false, "")
cmd.Flags().StringP("input", "i", "/dev/stdin", "")
original, err := ioutil.ReadFile(root + "/test.yaml")
if err != nil {
t.Fatalf("could not read file")
}
path, cleanup, err := tempTestYaml(root, "set-json", original)
if err != nil {
t.Fatal(err)
}
defer cleanup()
err = Set.Run(cmd, []string{path, "na-beira-da-lagoa"})
if err != nil {
t.Fatalf("Threw on good nested set: %s", err)
}
changed, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("could not read file")
}
if string(changed) == string(original) {
t.Fatal("Did not change file")
}
if !strings.Contains(string(changed), `
na-beira-da-lagoa:
foram: ensaiar
para: começar
`) {
t.Fatalf("Did not contain expected new entry tree, got:\n%s", changed)
}
}
func TestSetList(t *testing.T) {
logrus.SetLevel(logrus.DebugLevel)
root := fromProjectRoot()
Set.SetBindings()
out := bytes.Buffer{}
cmd := &cobra.Command{}
cmd.SetOut(&out)
cmd.SetErr(&out)
stdin := bytes.Buffer{}
stdin.Write([]byte("um"))
cmd.SetIn(&stdin)
Set.Cobra = cmd
cmd.Flags().Bool("secret", false, "")
cmd.Flags().Bool("delete", false, "")
cmd.Flags().Bool("json", false, "")
cmd.Flags().Bool("flush", false, "")
cmd.Flags().StringP("input", "i", "/dev/stdin", "")
original, err := ioutil.ReadFile(root + "/test.yaml")
if err != nil {
t.Fatalf("could not read file")
}
path, cleanup, err := tempTestYaml(root, "set-list-key", original)
if err != nil {
t.Fatal(err)
}
defer cleanup()
err = Set.Run(cmd, []string{path, "asdf.0"})
if err != nil {
t.Fatalf("Threw on good nested set: %s", err)
}
changed, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("could not read file")
}
if string(changed) == string(original) {
t.Fatal("Did not change file")
}
if !strings.Contains(string(changed), `
asdf:
- um
`) {
t.Fatalf("Did not contain expected new string, got:\n%s", changed)
}
}
func TestDelete(t *testing.T) {
root := fromProjectRoot()
Set.SetBindings()
out := bytes.Buffer{}
cmd := &cobra.Command{}
cmd.SetOut(&out)
cmd.SetErr(&out)
Set.Cobra = cmd
cmd.Flags().Bool("secret", false, "")
cmd.Flags().Bool("delete", true, "")
cmd.Flags().Bool("json", false, "")
cmd.Flags().Bool("flush", false, "")
cmd.Flags().StringP("input", "i", "/dev/stdin", "")
original, err := ioutil.ReadFile(root + "/test.yaml")
if err != nil {
t.Fatalf("could not read file")
}
path, cleanup, err := tempTestYaml(root, "set-delete-key", original)
if err != nil {
t.Fatal(err)
}
defer cleanup()
err = Set.Run(cmd, []string{path, "string", "--delete"})
if err != nil {
t.Fatalf("Threw on good set delete: %s", err)
}
changed, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("could not read file")
}
if string(changed) == string(original) {
t.Fatal("Did not change file")
}
if strings.Contains(string(changed), `
string: pato
`) {
t.Fatalf("Still contains deleted key, got:\n%s", changed)
}
}
func TestDeleteNested(t *testing.T) {
root := fromProjectRoot()
Set.SetBindings()
out := bytes.Buffer{}
cmd := &cobra.Command{}
cmd.SetOut(&out)
cmd.SetErr(&out)
Set.Cobra = cmd
cmd.Flags().Bool("secret", false, "")
cmd.Flags().Bool("delete", true, "")
cmd.Flags().Bool("json", false, "")
cmd.Flags().Bool("flush", false, "")
cmd.Flags().StringP("input", "i", "/dev/stdin", "")
original, err := ioutil.ReadFile(root + "/test.yaml")
if err != nil {
t.Fatalf("could not read file")
}
path, cleanup, err := tempTestYaml(root, "set-delete-nested-key", original)
if err != nil {
t.Fatal(err)
}
defer cleanup()
err = Set.Run(cmd, []string{path, "nested.string", "--delete"})
if err != nil {
t.Fatalf("Threw on good set delete nested: %s", err)
}
changed, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("could not read file")
}
if string(changed) == string(original) {
t.Fatal("Did not change file")
}
if strings.Contains(string(changed), `
string: quem
`) {
t.Fatalf("Still contains deleted nested key, got:\n%s", changed)
}
}

View File

@ -12,7 +12,7 @@ import (
var Plugin = &command.Command{
Path: []string{"vault", "server"},
Summary: "Starts a vault-joao-plugin server",
Description: `Runs joao as a vault plugin.
Description: `Runs joao as a vault plugin. See https://developer.hashicorp.com/vault/docs/plugins
You'll need to install joao in the machine running vault to plugin_directory as specified by vault's config. The installed joao executable needs to be executable for the user running vault only.

View File

@ -3,10 +3,14 @@
package opclient
import (
"strings"
"github.com/1Password/connect-sdk-go/connect"
op "github.com/1Password/connect-sdk-go/onepassword"
)
var ConnectClientFactory func(host, token, userAgent string) connect.Client = connect.NewClientWithUserAgent
// UUIDLength defines the required length of UUIDs.
const UUIDLength = 26
@ -33,7 +37,7 @@ type Connect struct {
const userAgent = "nidito-joao"
func NewConnect(host, token string) *Connect {
client := connect.NewClientWithUserAgent(host, token, userAgent)
client := ConnectClientFactory(host, token, userAgent)
return &Connect{client: client}
}
@ -41,12 +45,27 @@ func (b *Connect) Get(vault, name string) (*op.Item, error) {
return b.client.GetItem(name, vault)
}
func (b *Connect) Update(item *op.Item) error {
func (b *Connect) Update(item *op.Item, remote *op.Item) error {
_, err := b.client.UpdateItem(item, item.Vault.ID)
return err
}
func (b *Connect) List(vault, prefix string) ([]string, error) {
// TODO: get this done
return nil, nil
items, err := b.client.GetItems(vault)
if err != nil {
return nil, err
}
res := []string{}
for _, item := range items {
if prefix != "" && !strings.HasPrefix(item.Title, prefix) {
continue
}
res = append(res, item.Title)
}
return res, nil
}
func (b *Connect) Create(item *op.Item) error {
_, err := b.client.CreateItem(item, item.Vault.ID)
return err
}

View File

@ -89,6 +89,12 @@ func (cfg *Config) Set(path []string, data []byte, isSecret, parseEntry bool) er
if err := yaml.Unmarshal(data, newEntry); err != nil {
return err
}
if newEntry.Kind == yaml.MappingNode || newEntry.Kind == yaml.SequenceNode {
newEntry.Style = yaml.FoldedStyle | yaml.LiteralStyle
for _, v := range newEntry.Content {
v.Style = yaml.FlowStyle
}
}
} else {
valueStr = strings.Trim(valueStr, "\n")
if isSecret {
@ -108,12 +114,14 @@ func (cfg *Config) Set(path []string, data []byte, isSecret, parseEntry bool) er
entry := cfg.Tree
for idx, key := range path {
if len(path)-1 == idx {
dst := entry.ChildNamed(key)
if dst == nil {
if dst := entry.ChildNamed(key); dst == nil {
key := NewEntry(key, yaml.ScalarNode)
if entry.Kind == yaml.MappingNode {
key := NewEntry(key, yaml.ScalarNode)
logrus.Infof("setting new map key %v", newEntry.Path)
entry.Content = append(entry.Content, key, newEntry)
} else {
logrus.Infof("setting new list key %v", newEntry.Path)
entry.Kind = yaml.SequenceNode
entry.Content = append(entry.Content, newEntry)
}
} else {
@ -131,12 +139,15 @@ func (cfg *Config) Set(path []string, data []byte, isSecret, parseEntry bool) er
}
kind := yaml.MappingNode
if isNumeric(key) {
if idx+1 == len(path)-1 && isNumeric(path[idx+1]) {
kind = yaml.SequenceNode
}
sub := NewEntry(key, kind)
sub.Path = append(entry.Path, key) // nolint: gocritic
entry.Content = append(entry.Content, sub)
keyEntry := NewEntry(sub.Name(), yaml.ScalarNode)
keyEntry.Value = key
entry.Content = append(entry.Content, keyEntry, sub)
entry = sub
}

View File

@ -206,6 +206,10 @@ func (e *Entry) MarshalYAML() (*yaml.Node, error) {
}
} else {
entries := e.Contents()
if len(entries)%2 != 0 {
return nil, fmt.Errorf("cannot decode odd numbered contents list: %s", e.Path)
}
for i := 0; i < len(entries); i += 2 {
key := entries[i]
value := entries[i+1]
@ -213,6 +217,11 @@ func (e *Entry) MarshalYAML() (*yaml.Node, error) {
continue
}
if key.Type == "" {
key.Kind = yaml.ScalarNode
key.Type = "!!map"
}
keyNode, err := key.MarshalYAML()
if err != nil {
return nil, err
@ -288,11 +297,11 @@ func (e *Entry) FromOP(fields []*op.ItemField) error {
Type: kind,
}
if isNumeric(key) {
// logrus.Debugf("hydrating sequence value at %s", path)
logrus.Debugf("hydrating sequence value at %s", path)
container.Kind = yaml.SequenceNode
container.Content = append(container.Content, newEntry)
} else {
// logrus.Debugf("hydrating map value at %s", path)
logrus.Debugf("hydrating map value at %s", path)
keyEntry := NewEntry(key, yaml.ScalarNode)
keyEntry.Value = key
container.Content = append(container.Content, keyEntry, newEntry)
@ -303,25 +312,21 @@ func (e *Entry) FromOP(fields []*op.ItemField) error {
subContainer := container.ChildNamed(key)
if subContainer != nil {
container = subContainer
} else {
kind := yaml.MappingNode
if idx+1 < len(path)-1 && isNumeric(path[idx+1]) {
// logrus.Debugf("creating sequence container for key %s at %s", key, path)
kind = yaml.SequenceNode
}
child := NewEntry(key, kind)
child.Path = append(container.Path, key) // nolint: gocritic
if kind == yaml.SequenceNode {
container.Content = append(container.Content, child)
} else {
// logrus.Debugf("creating mapping container for %s at %s", key, container.Path)
keyEntry := NewEntry(child.Name(), yaml.ScalarNode)
keyEntry.Value = key
container.Content = append(container.Content, keyEntry, child)
}
container = child
continue
}
kind := yaml.MappingNode
if idx+1 == len(path)-1 && isNumeric(path[idx+1]) {
logrus.Debugf("creating sequence container for key %s at %s", key, path)
kind = yaml.SequenceNode
}
child := NewEntry(key, kind)
child.Path = append(container.Path, key) // nolint: gocritic
keyEntry := NewEntry(child.Name(), yaml.ScalarNode)
keyEntry.Value = key
container.Content = append(container.Content, keyEntry, child)
container = child
}
}
@ -332,7 +337,7 @@ func (e *Entry) ToOP() []*op.ItemField {
ret := []*op.ItemField{}
var section *op.ItemSection
if e.Kind == yaml.ScalarNode {
if e.IsScalar() {
name := e.Path[len(e.Path)-1]
fullPath := strings.Join(e.Path, ".")
if len(e.Path) > 1 {
@ -466,10 +471,8 @@ func (e *Entry) Merge(other *Entry) error {
if err := local.Merge(remote); err != nil {
return err
}
} else {
logrus.Debugf("adding new collection value at %s", remote.Path)
local.Content = append(local.Content, remote)
}
local.Content = append(local.Content, remote)
}
return nil

View File

@ -48,7 +48,11 @@ func Load(ref string, preferRemote bool) (*Config, error) {
return nil, fmt.Errorf("could not load %s from local as it's not a path", ref)
}
return FromFile(ref)
cfg, err := FromFile(ref)
if err != nil {
return nil, fmt.Errorf("could not load file %s: %w", ref, err)
}
return cfg, nil
}
// FromFile reads a path and returns a config.
@ -86,7 +90,7 @@ func FromYAML(data []byte) (*Config, error) {
err := yaml.Unmarshal(data, &cfg.Tree)
if err != nil {
return nil, err
return nil, fmt.Errorf("could not parse %w", err)
}
return cfg, nil

View File

@ -15,22 +15,6 @@ import (
"gopkg.in/yaml.v3"
)
func (cfg *Config) Lookup(query []string) (*Entry, error) {
if len(query) == 0 || len(query) == 1 && query[0] == "." {
return cfg.Tree, nil
}
entry := cfg.Tree
for _, part := range query {
entry = entry.ChildNamed(part)
if entry == nil {
return nil, fmt.Errorf("value not found at %s of %s", part, query)
}
}
return entry, nil
}
func findRepoConfig(from string) (*opDetails, error) {
parts := strings.Split(from, "/")
for i := len(parts); i > 0; i-- {
@ -107,7 +91,7 @@ func AutocompleteKeys(cmd *command.Command, currentValue, config string) ([]stri
keys, err := KeysFromYAML(buf)
if err != nil {
return nil, flag, err
return nil, flag, fmt.Errorf("could not parse file %s as %w", file, err)
}
sort.Strings(keys)

View File

@ -84,7 +84,7 @@ func (cfg *Config) ToOP() *op.Item {
continue
}
if value.Kind == yaml.MappingNode {
if value.Kind == yaml.MappingNode || value.Kind == yaml.SequenceNode {
sections = append(sections, &op.ItemSection{
ID: value.Name(),
Label: value.Name(),

View File

@ -4,7 +4,7 @@ _config: !!joao
# not sorted on purpose
int: 1 # line
# foot
string: "pato"
string: pato
bool: false
secret: !!secret very secret
nested:
@ -12,6 +12,10 @@ nested:
int: 1
secret: !!secret very secret
bool: true
list:
- 1
- 2
- 3
list:
- one
- two