gotextlog

gotextlog
git clone https://git.ryansepassi.com/git/gotextlog.git
Log | Files | Refs | README

config_test.go (6082B)


      1 package config
      2 
      3 import (
      4 	"encoding/json"
      5 	"os"
      6 	"path/filepath"
      7 	"runtime"
      8 	"strings"
      9 	"testing"
     10 )
     11 
     12 func TestConfigLifecycleAndEnvironmentPrecedence(t *testing.T) {
     13 	home := t.TempDir()
     14 	env := map[string]string{"HOME": home}
     15 	if runtime.GOOS == "windows" {
     16 		env["APPDATA"] = filepath.Join(home, "roaming")
     17 	} else if runtime.GOOS != "darwin" {
     18 		env["XDG_CONFIG_HOME"] = filepath.Join(home, "xdg")
     19 	}
     20 
     21 	stored := Config{
     22 		BaseURL: "https://stored.example",
     23 		Theme:   ThemeDark,
     24 		Token:   "stored-secret",
     25 		LastTab: FeedToMe,
     26 	}
     27 	if err := Save(stored, env); err != nil {
     28 		t.Fatalf("Save: %v", err)
     29 	}
     30 
     31 	path := ConfigPath(env)
     32 	info, err := os.Stat(path)
     33 	if err != nil {
     34 		t.Fatalf("Stat(%q): %v", path, err)
     35 	}
     36 	if runtime.GOOS != "windows" && info.Mode().Perm() != 0o600 {
     37 		t.Fatalf("config mode = %04o, want 0600", info.Mode().Perm())
     38 	}
     39 
     40 	loaded, err := Load(map[string]string{
     41 		"HOME":            home,
     42 		"APPDATA":         env["APPDATA"],
     43 		"XDG_CONFIG_HOME": env["XDG_CONFIG_HOME"],
     44 		"TEXTLOG_URL":     "https://override.example/",
     45 		"TEXTLOG_TOKEN":   "environment-secret",
     46 		"TEXTLOG_THEME":   "light",
     47 	})
     48 	if err != nil {
     49 		t.Fatalf("Load: %v", err)
     50 	}
     51 	want := Config{
     52 		BaseURL: "https://override.example",
     53 		Theme:   ThemeLight,
     54 		Token:   "environment-secret",
     55 		LastTab: FeedToMe,
     56 	}
     57 	if loaded != want {
     58 		t.Fatalf("Load = %#v, want %#v", loaded, want)
     59 	}
     60 
     61 	data, err := os.ReadFile(path)
     62 	if err != nil {
     63 		t.Fatal(err)
     64 	}
     65 	var unchanged Config
     66 	if err := json.Unmarshal(data, &unchanged); err != nil {
     67 		t.Fatalf("saved JSON: %v", err)
     68 	}
     69 	if unchanged != stored {
     70 		t.Fatalf("environment changed stored config to %#v", unchanged)
     71 	}
     72 	if !strings.HasSuffix(string(data), "\n") {
     73 		t.Fatalf("saved JSON has no trailing newline: %q", data)
     74 	}
     75 }
     76 
     77 func TestLoadDefaultsAndSanitizesStoredSelections(t *testing.T) {
     78 	home := t.TempDir()
     79 	env := isolatedEnv(home)
     80 	path := ConfigPath(env)
     81 	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
     82 		t.Fatal(err)
     83 	}
     84 	if err := os.WriteFile(path, []byte(`{"baseUrl":"https://stored.example/","theme":"sepia","token":"saved","lastTab":"live"}`), 0o600); err != nil {
     85 		t.Fatal(err)
     86 	}
     87 
     88 	got, err := Load(env)
     89 	if err != nil {
     90 		t.Fatal(err)
     91 	}
     92 	if got.BaseURL != "https://stored.example" || got.Theme != ThemeAuto || got.Token != "saved" || got.LastTab != "" {
     93 		t.Fatalf("Load invalid selections = %#v", got)
     94 	}
     95 
     96 	env["NO_COLOR"] = ""
     97 	env["TEXTLOG_THEME"] = "dark"
     98 	got, err = Load(env)
     99 	if err != nil {
    100 		t.Fatal(err)
    101 	}
    102 	if got.Theme != ThemeLight {
    103 		t.Fatalf("NO_COLOR theme = %q, want %q", got.Theme, ThemeLight)
    104 	}
    105 	if !got.NoColor {
    106 		t.Fatal("NO_COLOR was not retained for rendering")
    107 	}
    108 
    109 	missing, err := Load(isolatedEnv(t.TempDir()))
    110 	if err != nil {
    111 		t.Fatal(err)
    112 	}
    113 	if missing != (Config{BaseURL: DefaultURL, Theme: ThemeAuto}) {
    114 		t.Fatalf("missing config = %#v", missing)
    115 	}
    116 
    117 	malformedEnv := isolatedEnv(t.TempDir())
    118 	malformedPath := ConfigPath(malformedEnv)
    119 	if err := os.MkdirAll(filepath.Dir(malformedPath), 0o700); err != nil {
    120 		t.Fatal(err)
    121 	}
    122 	if err := os.WriteFile(malformedPath, []byte(`{"baseUrl":"https://partial.example",`), 0o600); err != nil {
    123 		t.Fatal(err)
    124 	}
    125 	malformed, err := Load(malformedEnv)
    126 	if err != nil {
    127 		t.Fatal(err)
    128 	}
    129 	if malformed != (Config{BaseURL: DefaultURL, Theme: ThemeAuto}) {
    130 		t.Fatalf("partially decoded malformed config = %#v", malformed)
    131 	}
    132 }
    133 
    134 func TestSavePersistsTheProvidedConfigWithoutApplyingEnvironment(t *testing.T) {
    135 	env := isolatedEnv(t.TempDir())
    136 	env["TEXTLOG_URL"] = "https://override.example"
    137 	env["TEXTLOG_THEME"] = "light"
    138 	want := Config{BaseURL: "https://stored.example/", Theme: ThemeDark, Token: "stored"}
    139 	if err := Save(want, env); err != nil {
    140 		t.Fatal(err)
    141 	}
    142 	data, err := os.ReadFile(ConfigPath(env))
    143 	if err != nil {
    144 		t.Fatal(err)
    145 	}
    146 	var got Config
    147 	if err := json.Unmarshal(data, &got); err != nil {
    148 		t.Fatal(err)
    149 	}
    150 	if got != want {
    151 		t.Fatalf("saved config = %#v, want %#v", got, want)
    152 	}
    153 }
    154 
    155 func TestNormalizeBaseURL(t *testing.T) {
    156 	valid := map[string]string{
    157 		"https://textlog.cc/":          "https://textlog.cc",
    158 		"http://localhost:3000/api///": "http://localhost:3000/api",
    159 		" https://EXAMPLE.test/a/ ":    "https://example.test/a",
    160 	}
    161 	for input, want := range valid {
    162 		got, err := NormalizeBaseURL(input)
    163 		if err != nil || got != want {
    164 			t.Errorf("NormalizeBaseURL(%q) = %q, %v; want %q", input, got, err, want)
    165 		}
    166 	}
    167 
    168 	for _, input := range []string{"wat", "file:///tmp/no", "https://example.test?q=one", "https://example.test/#frag", "https:///missing-host"} {
    169 		if _, err := NormalizeBaseURL(input); err == nil {
    170 			t.Errorf("NormalizeBaseURL(%q) unexpectedly succeeded", input)
    171 		}
    172 	}
    173 }
    174 
    175 func TestPlatformConfigPaths(t *testing.T) {
    176 	env := map[string]string{
    177 		"APPDATA":         filepath.FromSlash("/roaming"),
    178 		"XDG_CONFIG_HOME": filepath.FromSlash("/xdg"),
    179 	}
    180 	tests := map[string]string{
    181 		"windows": filepath.Join(env["APPDATA"], "textlog", "config.json"),
    182 		"darwin":  filepath.Join("/home/alice", "Library", "Application Support", "textlog", "config.json"),
    183 		"linux":   filepath.Join(env["XDG_CONFIG_HOME"], "textlog", "config.json"),
    184 	}
    185 	for goos, want := range tests {
    186 		if got := configPathFor(goos, "/home/alice", env); got != want {
    187 			t.Errorf("configPathFor(%q) = %q, want %q", goos, got, want)
    188 		}
    189 	}
    190 }
    191 
    192 func isolatedEnv(home string) map[string]string {
    193 	env := map[string]string{"HOME": home}
    194 	if runtime.GOOS == "windows" {
    195 		env["APPDATA"] = filepath.Join(home, "roaming")
    196 	} else if runtime.GOOS != "darwin" {
    197 		env["XDG_CONFIG_HOME"] = filepath.Join(home, "xdg")
    198 	}
    199 	return env
    200 }
    201 func TestLoadDoesNotTrustColorFGBG(t *testing.T) {
    202 	home := t.TempDir()
    203 	light, err := Load(map[string]string{"HOME": home, "XDG_CONFIG_HOME": home, "COLORFGBG": "0;15"})
    204 	if err != nil {
    205 		t.Fatal(err)
    206 	}
    207 	dark, err := Load(map[string]string{"HOME": home, "XDG_CONFIG_HOME": home, "COLORFGBG": "15;0"})
    208 	if err != nil {
    209 		t.Fatal(err)
    210 	}
    211 	if light != dark || light.Theme != ThemeAuto {
    212 		t.Fatalf("COLORFGBG changed automatic configuration: light=%#v dark=%#v", light, dark)
    213 	}
    214 }