main c21f374

config_test.go RawHistoryFolder

1package config
2
3import (
4	"github.com/bnema/neferafk/internal/ports"
5	"strings"
6	"testing"
7	"time"
8)
9
10func TestDefaultsAndIndependentOff(t *testing.T) {
11	c, err := Parse(strings.NewReader("# defaults\n"))
12	if err != nil || c != ports.Defaults() {
13		t.Fatal(c, err)
14	}
15	c, err = Parse(strings.NewReader("lock.after = off\nfade.after = 1s\nfade.duration = 0s\nscreens.off-after = off\nsleep.after = 24h\nauth.mode = pin\nauth.pin-source = env\nauth.pin-env = NEFERAFK_PIN\n"))
16	if err != nil || c.LockAfter != 0 || c.FadeAfter != time.Second || c.FadeDuration != 0 || c.SleepAfter != 24*time.Hour || c.Auth.PINEnv != "NEFERAFK_PIN" {
17		t.Fatal(c, err)
18	}
19}
20func TestRejectTransactionalBoundsUnknownDuplicateAndSecrets(t *testing.T) {
21	for _, text := range []string{"unknown = x", "lock.after=1s\nlock.after=2s", "lock.after=0s", "lock.after=-1s", "sleep.after=25h", "fade.duration=-1s", "fade.duration=off", "auth.password=false", "auth.pin-reference=x", "auth.mode=unknown", "auth.mode=pin", "auth.pin-source=pass\nauth.pin-entry=-option", "auth.pin-source=pass\nauth.pin-entry=a\x00b", "auth.pin=1234", "auth.pin-source=env\nauth.pin-env=SECRET=123", "auth.pin-source=pass\nauth.pin-entry=../secret", "auth.pin-source=pass\nauth.pin-entry=/secret", strings.Repeat("#", MaxLine+1), strings.Repeat("#\n", MaxBytes)} {
22		c, err := Parse(strings.NewReader(text))
23		if err == nil || c != (ports.Config{}) {
24			t.Fatalf("accepted/partial invalid input length %d", len(text))
25		}
26	}
27}
28func TestLockOutput(t *testing.T) {
29	c, err := Parse(strings.NewReader("lock.output = DP-2\n"))
30	if err != nil || c.LockOutput != "DP-2" {
31		t.Fatal(c, err)
32	}
33	if _, err := Parse(strings.NewReader("lock.output = " + strings.Repeat("x", ports.MaxOutputName+1))); err == nil {
34		t.Fatal("oversized output name accepted")
35	}
36	cfg := ports.Defaults()
37	cfg.LockOutput = "DP\x01"
38	if err := cfg.Validate(); err == nil {
39		t.Fatal("control character in output name accepted")
40	}
41}
42func TestPassReferenceMetadataOnly(t *testing.T) {
43	c, err := Parse(strings.NewReader("auth.pin-source=pass\nauth.pin-entry=desktop/neferafk\n"))
44	if err != nil || c.Auth.PINEntry != "desktop/neferafk" {
45		t.Fatal(c, err)
46	}
47}
48func TestDefaultPath(t *testing.T) {
49	t.Setenv("XDG_CONFIG_HOME", "/config")
50	if got := DefaultPath(); got != "/config/neferafk/config" {
51		t.Fatal(got)
52	}
53}
54
55func TestUnknownKeyDiagnosticCannotEchoSecret(t *testing.T) {
56	secret := "accidental-password-should-never-appear"
57	_, err := Parse(strings.NewReader(secret + " = " + secret))
58	if err == nil || strings.Contains(err.Error(), secret) {
59		t.Fatalf("unsafe diagnostic %v", err)
60	}
61}
62func TestRejectInvalidUTF8BeforeParsingAndDirectMetadata(t *testing.T) {
63	for _, text := range []string{"# \xff", "auth.pin-source=pass\nauth.pin-entry=a\xff"} {
64		if _, err := Parse(strings.NewReader(text)); err == nil {
65			t.Fatal("invalid UTF-8 parsed")
66		}
67	}
68	cfg := ports.Defaults()
69	cfg.Auth.PINSource = ports.PINSourcePass
70	cfg.Auth.PINEntry = "entry\xff"
71	if err := cfg.Validate(); err == nil {
72		t.Fatal("direct invalid UTF-8 metadata admitted")
73	}
74}
75
76func TestParseReportsFirstProblemInLineOrder(t *testing.T) {
77	long := strings.Repeat("#", MaxLine+1)
78	for _, tc := range []struct{ name, text, want string }{
79		{"bad value before missing =", "lock.after=bad\nfoo", "line 1: "},
80		{"bad value before duplicate", "lock.after=bad\nlock.after=1s", "line 1: "},
81		{"unknown key before missing =", "unknown=x\nnoeq", "line 1: unknown key"},
82		{"bad value before long line", "lock.after=bad\n" + long, "line 1: "},
83		{"missing = before bad value", "noeq\nlock.after=bad", "line 1: expected key = value"},
84		{"duplicate before later bad value", "lock.after=1s\nlock.after=2s\nsleep.after=bad", "line 2: overrides line 1"},
85		{"duplicate wins tie with its bad value", "lock.after=1s\nlock.after=bad", "line 2: overrides line 1"},
86	} {
87		t.Run(tc.name, func(t *testing.T) {
88			c, err := Parse(strings.NewReader(tc.text))
89			if err == nil || !strings.HasPrefix(err.Error(), tc.want) || c != (ports.Config{}) {
90				t.Fatalf("got %v, %+v; want error starting %q", err, c, tc.want)
91			}
92		})
93	}
94}
95
96func TestParseHashStartsCommentEvenInQuotes(t *testing.T) {
97	for _, tc := range []struct{ name, text, want string }{
98		{"plain", "lock.output = DP-2 # left", "DP-2"},
99		{"no space", "lock.output = DP-2#left", "DP-2"},
100		{"inside double quotes", `lock.output = "DP#2"`, `"DP`},
101		{"after unbalanced quote", `lock.output = DP-2 "# note`, `DP-2 "`},
102		{"inside single quotes", "lock.output = 'DP#2'", "'DP"},
103	} {
104		t.Run(tc.name, func(t *testing.T) {
105			c, err := Parse(strings.NewReader(tc.text))
106			if err != nil || c.LockOutput != tc.want {
107				t.Fatalf("got %q, %v; want %q", c.LockOutput, err, tc.want)
108			}
109		})
110	}
111	if _, err := Parse(strings.NewReader("lock.output = # only a comment")); err == nil {
112		t.Fatal("value made empty by a comment accepted")
113	}
114}
115
116// The kv tags on ports.Config repeat MaxDuration and the auth constants.
117func TestTagsMatchPortsConstants(t *testing.T) {
118	max := ports.MaxDuration.String()
119	for _, key := range []string{"lock.after", "fade.after", "screens.off-after", "sleep.after", "fade.duration"} {
120		if _, err := Parse(strings.NewReader(key + "=" + max)); err != nil {
121			t.Errorf("%s=%s rejected: %v", key, max, err)
122		}
123		if _, err := Parse(strings.NewReader(key + "=" + (ports.MaxDuration + 1).String())); err == nil {
124			t.Errorf("%s above %s accepted", key, max)
125		}
126	}
127	for _, text := range []string{
128		"auth.mode=" + string(ports.AuthModePassword),
129		"auth.mode=" + string(ports.AuthModePIN) + "\nauth.pin-source=" + string(ports.PINSourceEnv) + "\nauth.pin-env=PIN",
130		"auth.pin-source=" + string(ports.PINSourcePass) + "\nauth.pin-entry=a/b",
131	} {
132		if _, err := Parse(strings.NewReader(text)); err != nil {
133			t.Errorf("%q rejected: %v", text, err)
134		}
135	}
136}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 9

↑↓ moveenter openesc close