Skip to content

Commit

Permalink
introduce golden file test helper
Browse files Browse the repository at this point in the history
  • Loading branch information
strideynet committed May 10, 2022
1 parent 497995c commit 6e71633
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 26 deletions.
128 changes: 128 additions & 0 deletions lib/utils/golden/golden.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
Copyright 2022 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Golden files are a convenient way of storing data that we want to assert in
// unit tests. They are stored under the `testdata/` directory in a directory
// based on the name of the test. They are especially useful for storing large
// pieces of data that can be unwieldy to embed directly into your test tables.
//
// The convenience factor comes from the update mode which causes the tests to
// write data, rather than assert against it. This allows expected outputs
// to be updated easily when the underlying implementation is adjusted.
// This mode can be enabled by setting `GOLDEN_UPDATE=1` when running the tests
// you wish to update.
//
// Usage:
//
// Golden is ideal for testing the results of marshalling, or units that output
// large amounts of data to stdout or a file:
//
// func TestMarshalFooStruct(t *testing.T) {
// got, err := json.Marshal(FooStruct{Some: "Data"})
// require.NoError(t, err)
//
// if golden.Update() {
// golden.Set(t, got)
// }
// require.Equal(t, golden.Get(t), got)
// }
//
// It is possible to have multiple golden files per test using `GetNamed` and
// `SetNamed`. This is useful for cases where your unit under test produces
// multiple pieces of output e.g stdout and stderr:
//
// func TestFooCommand(t *testing.T) {
// stdoutBuf := new(bytes.Buffer)
// stderrBuf := new(bytes.Buffer)
//
// FooCommand(stdoutBuf, stderrBuf)
//
// stdout := stdoutBuf.Bytes()
// stderr := stderrBuf.Bytes()
//
// if golden.Update() {
// golden.SetNamed(t, "stdout", stdout)
// golden.SetNamed(t, "stderr", stderr)
// }
// require.Equal(t, golden.GetNamed(t, "stdout"), stdout)
// require.Equal(t, golden.GetNamed(t, "stderr"), stderr)
// }

package golden

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
)

func pathForFile(t *testing.T, name string) string {
pathComponents := []string{
"testdata",
t.Name(),
}

if name != "" {
pathComponents = append(pathComponents, name)
}

return filepath.Join(pathComponents...) + ".golden"
}

// ShouldSet provides a boolean value that indicates if your code should then
// call `Set` or `SetNamed` to update the stored golden file value with new
// data.
func ShouldSet() bool {
env := os.Getenv("GOLDEN_UPDATE")
return env == "y" || env == "1"
}

// SetNamed writes the supplied data to a named golden file for the current
// test.
func SetNamed(t *testing.T, name string, data []byte) {
p := pathForFile(t, name)
dir := filepath.Dir(p)

err := os.MkdirAll(dir, 0o755)
require.NoError(t, err)

err = os.WriteFile(p, data, 0o644)
require.NoError(t, err)
}

// Set writes the supplied data to the golden file for the current test.
func Set(t *testing.T, data []byte) {
SetNamed(t, "", data)
}

// GetNamed returns the contents of a named golden file for the current test. If
// the specified golden file does not exist for the test, the test will be
// failed.
func GetNamed(t *testing.T, name string) []byte {
p := pathForFile(t, name)
data, err := os.ReadFile(p)
require.NoError(t, err)

return data
}

// Get returns the contents of the golden file for the current test. If there is
// no golden file for the test, the test will be failed.
func Get(t *testing.T) []byte {
return GetNamed(t, "")
}
4 changes: 4 additions & 0 deletions tool/tbot/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ type CLIConf struct {
// Clean is a flag that, if set, instructs `tbot init` to remove existing
// unexpected files.
Clean bool

// ConfigureOutput provides a path that the generated configuration file
// should be written to
ConfigureOutput string
}

// OnboardingConfig contains values only required on first connect.
Expand Down
9 changes: 5 additions & 4 deletions tool/tbot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ const (
)

func main() {
if err := Run(os.Args[1:]); err != nil {
if err := Run(os.Args[1:], os.Stdout); err != nil {
utils.FatalError(err)
trace.DebugReport(err)
}
}

func Run(args []string) error {
func Run(args []string, stdout io.Writer) error {
var cf config.CLIConf
utils.InitLogger(utils.LoggingForDaemon, logrus.InfoLevel)

Expand Down Expand Up @@ -94,6 +94,7 @@ func Run(args []string) error {
configureCmd.Flag("oneshot", "If set, quit after the first renewal.").BoolVar(&cf.Oneshot)
configureCmd.Flag("renewal-interval", "Interval at which short-lived certificates are renewed; must be less than the certificate TTL.").DurationVar(&cf.RenewalInterval)
configureCmd.Flag("token", "A bot join token, if attempting to onboard a new bot; used on first connect.").Envar(tokenEnvVar).StringVar(&cf.Token)
configureCmd.Flag("output", "Path to write the generated configuration file to rather than write to stdout.").Short('o').StringVar(&cf.ConfigureOutput)

watchCmd := app.Command("watch", "Watch a destination directory for changes.").Hidden()

Expand All @@ -120,7 +121,7 @@ func Run(args []string) error {
case startCmd.FullCommand():
err = onStart(botConfig)
case configureCmd.FullCommand():
err = onConfigure(cf, os.Stdout)
err = onConfigure(cf, stdout)
case initCmd.FullCommand():
err = onInit(botConfig, &cf)
case watchCmd.FullCommand():
Expand All @@ -143,7 +144,7 @@ func onConfigure(
stdout io.Writer,
) error {
out := stdout
outPath := cf.ConfigPath
outPath := cf.ConfigureOutput
if outPath != "" {
f, err := os.Create(outPath)
if err != nil {
Expand Down
109 changes: 87 additions & 22 deletions tool/tbot/main_test.go
Original file line number Diff line number Diff line change
@@ -1,42 +1,107 @@
/*
Copyright 2022 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main

import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"

"github.com/gravitational/teleport/tool/tbot/config"
"github.com/gravitational/teleport/lib/utils/golden"
"github.com/stretchr/testify/require"
)

func Test_onConfigure(t *testing.T) {
func TestRun_Configure(t *testing.T) {
t.Parallel()

cfg := &config.BotConfig{
AuthServer: "foo:bar",
// This is slightly rubbish, but due to the global nature of `botfs`,
// it's difficult to configure the default acl and symlink values to be
// the same across dev laptops and GCB.
// If we switch to a more dependency injected model for botfs, we can
// ensure that the test one returns the same value across operating systems.
normalizeOSDependentValues := func(data []byte) []byte {
str := string(data)
str = strings.ReplaceAll(
str, "symlinks: try-secure", "symlinks: secure",
)
str = strings.ReplaceAll(
str, `acls: "off"`, "acls: try",
)
return []byte(str)
}
// TODO: It would be nice to pull this out into a golden file
expect := "debug: false\nauth_server: foo:bar\ncertificate_ttl: 0s\nrenewal_interval: 0s\noneshot: false\n"

t.Run("file", func(t *testing.T) {
t.Parallel()
baseArgs := []string{"configure"}
tests := []struct {
name string
args []string
}{
{
name: "no parameters provided",
args: baseArgs,
},
{
name: "all parameters provided",
args: append(baseArgs, []string{
"-a", "example.com",
"--token", "xxyzz",
"--ca-pin", "sha256:capindata",
"--data-dir", "/custom/data/dir",
"--join-method", "token",
"--oneshot",
"--certificate-ttl", "42m",
"--renewal-interval", "21m",
}...),
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
t.Run("file", func(t *testing.T) {
t.Parallel()

path := filepath.Join(t.TempDir(), "config.yaml")
err := onConfigure(&config.BotConfig{}, path)
require.NoError(t, err)
path := filepath.Join(t.TempDir(), "config.yaml")
args := append(tt.args, []string{"-o", path}...)
err := Run(args, nil)
require.NoError(t, err)

data, err := os.ReadFile(path)
require.NoError(t, err)
require.Equal(t, expect, string(data))
})
data, err := os.ReadFile(path)
data = normalizeOSDependentValues(data)
require.NoError(t, err)
if golden.ShouldSet() {
golden.Set(t, data)
}
require.Equal(t, string(golden.Get(t)), string(data))
})

t.Run("stdout", func(t *testing.T) {
t.Parallel()
t.Run("stdout", func(t *testing.T) {
t.Parallel()

stdout := new(bytes.Buffer)
err := onConfigure(stdout, "")
require.NoError(t, err)
require.Equal(t, expect, stdout.String())
})
stdout := new(bytes.Buffer)
err := Run(tt.args, stdout)
require.NoError(t, err)
data := normalizeOSDependentValues(stdout.Bytes())
if golden.ShouldSet() {
golden.Set(t, data)
}
require.Equal(t, string(golden.Get(t)), string(data))
})
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# tbot config file generated by `configure` command
onboarding:
token: xxyzz
ca_path: ""
ca_pins:
- sha256:capindata
join_method: token
storage:
directory:
path: /custom/data/dir
symlinks: secure
acls: try
debug: false
auth_server: example.com
certificate_ttl: 42m0s
renewal_interval: 21m0s
oneshot: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# tbot config file generated by `configure` command
onboarding:
token: xxyzz
ca_path: ""
ca_pins:
- sha256:capindata
join_method: token
storage:
directory:
path: /custom/data/dir
symlinks: secure
acls: try
debug: false
auth_server: example.com
certificate_ttl: 42m0s
renewal_interval: 21m0s
oneshot: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# tbot config file generated by `configure` command
storage:
directory:
path: /var/lib/teleport/bot
symlinks: secure
acls: try
debug: false
auth_server: ""
certificate_ttl: 1h0m0s
renewal_interval: 20m0s
oneshot: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# tbot config file generated by `configure` command
storage:
directory:
path: /var/lib/teleport/bot
symlinks: secure
acls: try
debug: false
auth_server: ""
certificate_ttl: 1h0m0s
renewal_interval: 20m0s
oneshot: false

0 comments on commit 6e71633

Please sign in to comment.