Added unit tests for CLI
This commit is contained in:
1 parent
169c6c6b64
commit
5ec4b65b91
7 files changed
+965
No files matched your search
@@ -0,0 +1,208 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.avsdev.uk/Go/job-search/internal/logging"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// The commands themselves are tested through the real command tree in
|
||||
// cmd/jobsearch. These tests cover what that can't reach easily: switching
|
||||
// to file logging, and Confirm's answers.
|
||||
|
||||
// setupDir changes into a new temporary directory holding an empty .env and
|
||||
// config.yaml with the given contents, and isolates Viper and logging.
|
||||
func setupDir(t *testing.T, config string) {
|
||||
t.Helper()
|
||||
t.Chdir(t.TempDir())
|
||||
writeFile(t, ".env", "")
|
||||
writeFile(t, "config.yaml", config)
|
||||
|
||||
viper.Reset()
|
||||
oldLog := slog.Default()
|
||||
t.Cleanup(func() {
|
||||
viper.Reset()
|
||||
if err := logging.Shutdown(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
slog.SetDefault(oldLog)
|
||||
logging.SetLevel(slog.LevelInfo)
|
||||
})
|
||||
}
|
||||
|
||||
func writeFile(t *testing.T, path, contents string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// newCommand returns a command with the shared flags bound to opts, as the
|
||||
// root command has, after parsing args.
|
||||
func newCommand(t *testing.T, opts *Options, args ...string) *cobra.Command {
|
||||
t.Helper()
|
||||
cmd := &cobra.Command{Use: "test"}
|
||||
opts.Bind(cmd)
|
||||
if err := cmd.ParseFlags(args); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func TestRunSetupSwitchesToLogFile(t *testing.T) {
|
||||
setupDir(t, "logging:\n level: info\n file: ./app.log\n")
|
||||
opts := &Options{}
|
||||
cmd := newCommand(t, opts)
|
||||
|
||||
conf, err := opts.RunSetup(cmd, true, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conf.Logging.File != "./app.log" {
|
||||
t.Errorf("Logging.File = %q", conf.Logging.File)
|
||||
}
|
||||
if !cmd.SilenceUsage {
|
||||
t.Error("RunSetup did not silence usage for runtime errors")
|
||||
}
|
||||
if !logging.IsFileLogger() {
|
||||
t.Fatal("logging was not switched to the log file")
|
||||
}
|
||||
|
||||
slog.Info("written to the log file")
|
||||
if err := opts.Finish(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if logging.IsFileLogger() {
|
||||
t.Error("Finish left the log file open")
|
||||
}
|
||||
b, err := os.ReadFile("app.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(b), "written to the log file") {
|
||||
t.Errorf("app.log = %q", b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSetupWithoutLogFileStaysOnTerminal(t *testing.T) {
|
||||
setupDir(t, "logging:\n level: info\n")
|
||||
opts := &Options{}
|
||||
|
||||
if _, err := opts.RunSetup(newCommand(t, opts), true, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if logging.IsFileLogger() {
|
||||
t.Error("switched to file logging with no log file configured")
|
||||
}
|
||||
if err := opts.Finish(); err != nil {
|
||||
t.Errorf("Finish without a log file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSetupTerminalLogIgnoresLogFile(t *testing.T) {
|
||||
setupDir(t, "logging:\n file: ./app.log\n")
|
||||
opts := &Options{}
|
||||
|
||||
if _, err := opts.RunSetup(newCommand(t, opts), true, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if logging.IsFileLogger() {
|
||||
t.Error("switched to file logging when terminal logging was asked for")
|
||||
}
|
||||
if _, err := os.Stat("app.log"); err == nil {
|
||||
t.Error("app.log was created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSetupBadLogFile(t *testing.T) {
|
||||
setupDir(t, "logging:\n file: ./missing-dir/app.log\n")
|
||||
opts := &Options{}
|
||||
|
||||
_, err := opts.RunSetup(newCommand(t, opts), true, false)
|
||||
if err == nil || !strings.Contains(err.Error(), "opening log file") {
|
||||
t.Errorf("err = %v, want opening log file", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Without loading, the configuration only reflects the flags.
|
||||
func TestRunSetupWithoutLoadingConfig(t *testing.T) {
|
||||
setupDir(t, "logging:\n level: info\n")
|
||||
|
||||
for args, want := range map[string]string{
|
||||
"": "",
|
||||
"--debug": "debug",
|
||||
"--quiet": "error",
|
||||
"-d -q": "debug", // --debug wins
|
||||
} {
|
||||
opts := &Options{}
|
||||
conf, err := opts.RunSetup(newCommand(t, opts, strings.Fields(args)...), false, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conf.Logging.Level != want {
|
||||
t.Errorf("%q: Logging.Level = %q, want %q", args, conf.Logging.Level, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupInitialLogging(t *testing.T) {
|
||||
setupDir(t, "")
|
||||
for args, wantDebug := range map[string]bool{
|
||||
"--quiet": false,
|
||||
"--debug": true,
|
||||
"-q -d": true, // --debug wins
|
||||
} {
|
||||
logging.SetLevel(slog.LevelInfo)
|
||||
SetupInitialLogging(newCommand(t, &Options{}, strings.Fields(args)...))
|
||||
if got := logging.IsDebugEnabled(); got != wantDebug {
|
||||
t.Errorf("%q: debug enabled = %v, want %v", args, got, wantDebug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirm(t *testing.T) {
|
||||
for input, want := range map[string]bool{
|
||||
"y\n": true,
|
||||
"YES\n": true,
|
||||
" Yes \n": true,
|
||||
"n\n": false,
|
||||
"No\n": false,
|
||||
"maybe\n\ny\n": true, // asks again until it gets an answer
|
||||
"what?\nno\nyes\n": false,
|
||||
} {
|
||||
if got := confirmWithInput(t, input); got != want {
|
||||
t.Errorf("Confirm with input %q = %v, want %v", input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// confirmWithInput runs Confirm with input as standard input, discarding the
|
||||
// prompt it prints.
|
||||
func confirmWithInput(t *testing.T, input string) bool {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
in := filepath.Join(dir, "stdin")
|
||||
writeFile(t, in, input)
|
||||
stdin, err := os.Open(filepath.Clean(in))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stdin.Close()
|
||||
stdout, err := os.Create(filepath.Clean(filepath.Join(dir, "stdout")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stdout.Close()
|
||||
|
||||
oldIn, oldOut := os.Stdin, os.Stdout
|
||||
os.Stdin, os.Stdout = stdin, stdout
|
||||
defer func() { os.Stdin, os.Stdout = oldIn, oldOut }()
|
||||
|
||||
return Confirm("Proceed?")
|
||||
}
|
||||
Reference in new issue
Block a user