init
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
package clicmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/iceBear67/simplepages/internal/cliutil"
|
||||
)
|
||||
|
||||
// clearEnv makes a test independent of whatever the developer has exported.
|
||||
func clearEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
for _, k := range []string{
|
||||
"PAGES_SERVER", "PAGES_TOKEN", "PAGES_TOKEN_FILE",
|
||||
"PAGES_PROJECT", "PAGES_OUTPUT", "PAGES_TIMEOUT", "PAGES_CONFIG",
|
||||
} {
|
||||
t.Setenv(k, "")
|
||||
}
|
||||
}
|
||||
|
||||
// writeConfig returns the path to a config file holding f.
|
||||
func writeConfig(t *testing.T, f ConfigFile) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "config.json")
|
||||
if err := SaveConfig(path, f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// newTestGlobals is what NewGlobals would give a command, with the streams
|
||||
// captured and the config file pinned so no real one can interfere.
|
||||
func newTestGlobals(config string) (*Globals, *strings.Builder, *strings.Builder) {
|
||||
var out, errOut strings.Builder
|
||||
return &Globals{In: strings.NewReader(""), Out: &out, Err: &errOut, Config: config}, &out, &errOut
|
||||
}
|
||||
|
||||
// TestPrecedence is the rule stated in the help text: flag, then environment,
|
||||
// then config file, then default. It is easy to get subtly wrong, and wrong
|
||||
// here means a CI job deploying to the wrong server.
|
||||
func TestPrecedence(t *testing.T) {
|
||||
file := ConfigFile{Server: "https://file.example.com", Project: "fileproj", Output: "json"}
|
||||
|
||||
t.Run("flag wins", func(t *testing.T) {
|
||||
clearEnv(t)
|
||||
t.Setenv("PAGES_SERVER", "https://env.example.com")
|
||||
t.Setenv("PAGES_PROJECT", "envproj")
|
||||
g, _, _ := newTestGlobals(writeConfig(t, file))
|
||||
g.Server, g.Project = "https://flag.example.com", "flagproj"
|
||||
if err := g.Resolve(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g.Server != "https://flag.example.com" || g.Project != "flagproj" {
|
||||
t.Errorf("server=%q project=%q, want the flag values", g.Server, g.Project)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("environment beats the config file", func(t *testing.T) {
|
||||
clearEnv(t)
|
||||
t.Setenv("PAGES_SERVER", "https://env.example.com")
|
||||
t.Setenv("PAGES_PROJECT", "envproj")
|
||||
g, _, _ := newTestGlobals(writeConfig(t, file))
|
||||
if err := g.Resolve(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g.Server != "https://env.example.com" || g.Project != "envproj" {
|
||||
t.Errorf("server=%q project=%q, want the environment values", g.Server, g.Project)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the config file is the last word before defaults", func(t *testing.T) {
|
||||
clearEnv(t)
|
||||
g, _, _ := newTestGlobals(writeConfig(t, file))
|
||||
if err := g.Resolve(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g.Server != "https://file.example.com" || g.Project != "fileproj" || g.Output != "json" {
|
||||
t.Errorf("server=%q project=%q output=%q, want the file values", g.Server, g.Project, g.Output)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("defaults fill the rest", func(t *testing.T) {
|
||||
clearEnv(t)
|
||||
g, _, _ := newTestGlobals(filepath.Join(t.TempDir(), "absent.json"))
|
||||
if err := g.Resolve(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g.Output != cliutil.FormatTable {
|
||||
t.Errorf("output = %q, want %q", g.Output, cliutil.FormatTable)
|
||||
}
|
||||
if g.Timeout <= 0 {
|
||||
t.Errorf("timeout = %v, want a positive default", g.Timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFlagsSurviveTheSubcommandParse covers the trap that makes this design
|
||||
// work: every global flag is re-registered on each subcommand's FlagSet, and
|
||||
// registering with a fixed default would wipe a value given before the
|
||||
// subcommand name.
|
||||
func TestFlagsSurviveTheSubcommandParse(t *testing.T) {
|
||||
clearEnv(t)
|
||||
cfg := writeConfig(t, ConfigFile{})
|
||||
|
||||
for _, args := range [][]string{
|
||||
{"--server", "https://flag.example.com", "--config", cfg, "probe", "demo"},
|
||||
{"probe", "--server", "https://flag.example.com", "--config", cfg, "demo"},
|
||||
{"--server", "https://flag.example.com", "probe", "demo", "--config", cfg},
|
||||
} {
|
||||
t.Run(strings.Join(args, " "), func(t *testing.T) {
|
||||
g, _, _ := newTestGlobals("")
|
||||
var gotProject string
|
||||
root := &cliutil.Command{
|
||||
Name: "pages",
|
||||
Sub: []*cliutil.Command{{
|
||||
Name: "probe",
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
if err := g.Resolve(); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(args) == 1 {
|
||||
gotProject = args[0]
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}},
|
||||
}
|
||||
if err := cliutil.Run(context.Background(), root, args, io.Discard, g.Register); err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
if g.Server != "https://flag.example.com" {
|
||||
t.Errorf("server = %q, want the flag to survive dispatch", g.Server)
|
||||
}
|
||||
if gotProject != "demo" {
|
||||
t.Errorf("positional = %q, want demo", gotProject)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenPrecedence(t *testing.T) {
|
||||
const (
|
||||
flagTok = "pgs_flagflagflagfl_secret"
|
||||
flagFileTok = "pgs_flagfileflagfi_secret"
|
||||
envTok = "pgs_envenvenvenven_secret"
|
||||
envFileTok = "pgs_envfileenvfile_secret"
|
||||
fileTok = "pgs_fileconfigfile_secret"
|
||||
)
|
||||
tokenFile := func(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
p := filepath.Join(t.TempDir(), "token")
|
||||
if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
t.Run("--token wins but warns", func(t *testing.T) {
|
||||
clearEnv(t)
|
||||
t.Setenv("PAGES_TOKEN", envTok)
|
||||
g, _, errOut := newTestGlobals(writeConfig(t, ConfigFile{Token: fileTok}))
|
||||
g.Token = flagTok
|
||||
g.TokenFile = tokenFile(t, flagFileTok)
|
||||
if err := g.Resolve(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g.Token != flagTok {
|
||||
t.Errorf("token = %q, want the flag", g.Token)
|
||||
}
|
||||
// argv is world-readable through /proc on a shared runner, so this
|
||||
// warning is the whole reason --token is documented as a last resort.
|
||||
if !strings.Contains(errOut.String(), "process list") {
|
||||
t.Errorf("stderr = %q, want a warning about the process list", errOut.String())
|
||||
}
|
||||
if strings.Contains(errOut.String(), "secret") {
|
||||
t.Error("the warning printed the token")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("--token-file beats the environment", func(t *testing.T) {
|
||||
clearEnv(t)
|
||||
t.Setenv("PAGES_TOKEN", envTok)
|
||||
g, _, _ := newTestGlobals(writeConfig(t, ConfigFile{Token: fileTok}))
|
||||
g.TokenFile = tokenFile(t, flagFileTok)
|
||||
if err := g.Resolve(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g.Token != flagFileTok {
|
||||
t.Errorf("token = %q, want the one from --token-file", g.Token)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PAGES_TOKEN beats PAGES_TOKEN_FILE", func(t *testing.T) {
|
||||
clearEnv(t)
|
||||
t.Setenv("PAGES_TOKEN", envTok)
|
||||
t.Setenv("PAGES_TOKEN_FILE", tokenFile(t, envFileTok))
|
||||
g, _, _ := newTestGlobals(writeConfig(t, ConfigFile{Token: fileTok}))
|
||||
if err := g.Resolve(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g.Token != envTok {
|
||||
t.Errorf("token = %q, want PAGES_TOKEN", g.Token)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PAGES_TOKEN_FILE beats the config file", func(t *testing.T) {
|
||||
clearEnv(t)
|
||||
t.Setenv("PAGES_TOKEN_FILE", tokenFile(t, envFileTok))
|
||||
g, _, _ := newTestGlobals(writeConfig(t, ConfigFile{Token: fileTok}))
|
||||
if err := g.Resolve(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g.Token != envFileTok {
|
||||
t.Errorf("token = %q, want PAGES_TOKEN_FILE", g.Token)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the config file is the fallback", func(t *testing.T) {
|
||||
clearEnv(t)
|
||||
g, _, _ := newTestGlobals(writeConfig(t, ConfigFile{Token: fileTok}))
|
||||
if err := g.Resolve(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g.Token != fileTok {
|
||||
t.Errorf("token = %q, want the config file's", g.Token)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestTokenFileTrimsTrailingNewline: `pages key create ... > token` and every
|
||||
// text editor add one.
|
||||
func TestTokenFileTrimsTrailingNewline(t *testing.T) {
|
||||
clearEnv(t)
|
||||
p := filepath.Join(t.TempDir(), "token")
|
||||
os.WriteFile(p, []byte("pgs_abcdefghijklmnop_secret\n"), 0o600)
|
||||
|
||||
g, _, _ := newTestGlobals(filepath.Join(t.TempDir(), "absent.json"))
|
||||
g.TokenFile = p
|
||||
if err := g.Resolve(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g.Token != "pgs_abcdefghijklmnop_secret" {
|
||||
t.Errorf("token = %q, want the newline trimmed", g.Token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenFileProblemsAreReported(t *testing.T) {
|
||||
t.Run("missing", func(t *testing.T) {
|
||||
clearEnv(t)
|
||||
g, _, _ := newTestGlobals(filepath.Join(t.TempDir(), "absent.json"))
|
||||
g.TokenFile = filepath.Join(t.TempDir(), "nope")
|
||||
if err := g.Resolve(); err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
clearEnv(t)
|
||||
p := filepath.Join(t.TempDir(), "token")
|
||||
os.WriteFile(p, []byte("\n\n"), 0o600)
|
||||
g, _, _ := newTestGlobals(filepath.Join(t.TempDir(), "absent.json"))
|
||||
g.TokenFile = p
|
||||
err := g.Resolve()
|
||||
if err == nil || !strings.Contains(err.Error(), "empty") {
|
||||
t.Fatalf("err = %v, want it to say the file is empty", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("world readable warns", func(t *testing.T) {
|
||||
clearEnv(t)
|
||||
p := filepath.Join(t.TempDir(), "token")
|
||||
os.WriteFile(p, []byte("pgs_abcdefghijklmnop_secret"), 0o644)
|
||||
g, _, errOut := newTestGlobals(filepath.Join(t.TempDir(), "absent.json"))
|
||||
g.TokenFile = p
|
||||
if err := g.Resolve(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(errOut.String(), "chmod 600") {
|
||||
t.Errorf("stderr = %q, want a mode warning", errOut.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolveRejectsUnknownOutputFormat(t *testing.T) {
|
||||
clearEnv(t)
|
||||
g, _, _ := newTestGlobals(filepath.Join(t.TempDir(), "absent.json"))
|
||||
g.Output = "yaml"
|
||||
err := g.Resolve()
|
||||
if err == nil || !strings.Contains(err.Error(), "table or json") {
|
||||
t.Fatalf("err = %v, want it to list the formats", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveParsesFriendlyTimeouts(t *testing.T) {
|
||||
clearEnv(t)
|
||||
t.Setenv("PAGES_TIMEOUT", "2m")
|
||||
g, _, _ := newTestGlobals(filepath.Join(t.TempDir(), "absent.json"))
|
||||
if err := g.Resolve(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g.Timeout.Minutes() != 2 {
|
||||
t.Errorf("timeout = %v, want 2m", g.Timeout)
|
||||
}
|
||||
|
||||
clearEnv(t)
|
||||
t.Setenv("PAGES_TIMEOUT", "later")
|
||||
g2, _, _ := newTestGlobals(filepath.Join(t.TempDir(), "absent.json"))
|
||||
err := g2.Resolve()
|
||||
if err == nil || !strings.Contains(err.Error(), "PAGES_TIMEOUT") {
|
||||
t.Fatalf("err = %v, want it to name the variable", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectNameExplainsHowToSetIt(t *testing.T) {
|
||||
clearEnv(t)
|
||||
g, _, _ := newTestGlobals(filepath.Join(t.TempDir(), "absent.json"))
|
||||
_, err := g.ProjectName()
|
||||
if err == nil || !strings.Contains(err.Error(), "PAGES_PROJECT") {
|
||||
t.Fatalf("err = %v, want it to name the flag and the variable", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClientNeedsServerAndToken: the two settings a fresh CI job forgets.
|
||||
func TestClientNeedsServerAndToken(t *testing.T) {
|
||||
clearEnv(t)
|
||||
g, _, _ := newTestGlobals(filepath.Join(t.TempDir(), "absent.json"))
|
||||
if _, err := g.Client(); err == nil || !strings.Contains(err.Error(), "PAGES_SERVER") {
|
||||
t.Fatalf("err = %v, want it to name PAGES_SERVER", err)
|
||||
}
|
||||
|
||||
clearEnv(t)
|
||||
g2, _, _ := newTestGlobals(filepath.Join(t.TempDir(), "absent.json"))
|
||||
g2.Server = "https://p.example.com"
|
||||
if _, err := g2.Client(); err == nil || !strings.Contains(err.Error(), "PAGES_TOKEN") {
|
||||
t.Fatalf("err = %v, want it to name PAGES_TOKEN", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user