209 lines
5.3 KiB
Go
209 lines
5.3 KiB
Go
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?")
|
|
}
|