refactor/kvconf 3421f16

logind_test.go RawHistoryFolder

1package logind
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"os"
8	"testing"
9	"time"
10
11	"github.com/bnema/neferafk/internal/ports"
12	"github.com/godbus/dbus/v5"
13	"github.com/stretchr/testify/mock"
14	"golang.org/x/sys/unix"
15)
16
17const testOwner = ":1.42"
18const testSession dbus.ObjectPath = "/org/freedesktop/login1/session/c1"
19
20func expectProbe(t *testing.T, b *mockbus, uid uint32) {
21	t.Helper()
22	bounded := mock.MatchedBy(func(ctx context.Context) bool { d, ok := ctx.Deadline(); return ok && time.Until(d) <= callTimeout })
23	b.EXPECT().call(bounded, "org.freedesktop.DBus", dbus.ObjectPath("/org/freedesktop/DBus"), "org.freedesktop.DBus.GetNameOwner", []any{service}).Return([]any{testOwner}, nil).Once()
24	b.EXPECT().call(bounded, testOwner, managerPath, managerInterface+".GetSessionByPID", []any{uint32(os.Getpid())}).Return([]any{testSession}, nil).Once()
25	b.EXPECT().call(bounded, testOwner, testSession, "org.freedesktop.DBus.Properties.Get", []any{sessionInterface, "User"}).Return([]any{dbus.MakeVariant([]any{uid, dbus.ObjectPath("/org/freedesktop/login1/user/_1000")})}, nil).Once()
26}
27func TestProbeCurrentPIDRealUIDAndReadOnly(t *testing.T) {
28	b := newMockbus(t)
29	expectProbe(t, b, uint32(os.Getuid()))
30	b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".CanSuspend").Return([]any{"yes"}, nil).Once()
31	b.EXPECT().call(mock.Anything, testOwner, managerPath, "org.freedesktop.DBus.Properties.Get", []any{managerInterface, "InhibitDelayMaxUSec"}).Return([]any{dbus.MakeVariant(uint64(5_000_000))}, nil).Once()
32	b.EXPECT().unixFDs().Return(true).Once()
33	a := newAdapter(b, ports.SystemRequirements{Session: true, Sleep: true})
34	if err := a.Probe(context.Background()); err != nil {
35		t.Fatal(err)
36	}
37	if c := a.Capabilities(); !c.Session || !c.Suspend || c.DelayMax != 5*time.Second {
38		t.Fatalf("capabilities %+v", c)
39	}
40	if path, source := a.Session(); path != string(testSession) || source != "pid" {
41		t.Fatalf("session %q source %q", path, source)
42	}
43	// No mutating call was configured: generated mock rejects Inhibit/Suspend.
44}
45
46const testUser dbus.ObjectPath = "/org/freedesktop/login1/user/_1000"
47const staleSession dbus.ObjectPath = "/org/freedesktop/login1/session/c9"
48
49var errPID, errXDG, errUser = errors.New("pid lookup"), errors.New("xdg lookup"), errors.New("user lookup")
50
51// expectNoPIDSession answers the owner lookup and fails the PID session.
52func expectNoPIDSession(b *mockbus) {
53	b.EXPECT().call(mock.Anything, "org.freedesktop.DBus", dbus.ObjectPath("/org/freedesktop/DBus"), "org.freedesktop.DBus.GetNameOwner", []any{service}).Return([]any{testOwner}, nil).Once()
54	b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".GetSessionByPID", []any{uint32(os.Getpid())}).Return(nil, errPID).Once()
55}
56func expectSessionProperty(b *mockbus, path dbus.ObjectPath, name string, value any) {
57	b.EXPECT().call(mock.Anything, testOwner, path, "org.freedesktop.DBus.Properties.Get", []any{sessionInterface, name}).Return([]any{dbus.MakeVariant(value)}, nil).Once()
58}
59
60// expectSessionKind answers Type, then State only when Type is graphical.
61func expectSessionKind(b *mockbus, path dbus.ObjectPath, typ, state string) {
62	expectSessionProperty(b, path, "Type", typ)
63	if typ == "wayland" || typ == "x11" {
64		expectSessionProperty(b, path, "State", state)
65	}
66}
67func expectSessionUID(b *mockbus, path dbus.ObjectPath, uid uint32) {
68	expectSessionProperty(b, path, "User", []any{uid, testUser})
69}
70
71// expectDisplay answers the GetUser + User.Display fallback with display.
72func expectDisplay(b *mockbus, display any) {
73	b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".GetUser", []any{uint32(os.Getuid())}).Return([]any{testUser}, nil).Once()
74	b.EXPECT().call(mock.Anything, testOwner, testUser, "org.freedesktop.DBus.Properties.Get", []any{userInterface, "Display"}).Return([]any{dbus.MakeVariant(display)}, nil).Once()
75}
76
77// A compositor run as a systemd user service gives its children no PID
78// session; the display manager's live graphical XDG_SESSION_ID is used,
79// UID-checked.
80func TestProbeFallsBackToXDGSessionID(t *testing.T) {
81	t.Setenv("XDG_SESSION_ID", "c1")
82	for name, uid := range map[string]uint32{"same uid": uint32(os.Getuid()), "other uid": uint32(os.Getuid()) + 1} {
83		t.Run(name, func(t *testing.T) {
84			b := newMockbus(t)
85			expectNoPIDSession(b)
86			b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".GetSession", []any{"c1"}).Return([]any{testSession}, nil).Once()
87			expectSessionKind(b, testSession, "wayland", "active")
88			expectSessionUID(b, testSession, uid)
89			a := newAdapter(b, ports.SystemRequirements{Session: true})
90			err := a.Probe(context.Background())
91			if ok := uid == uint32(os.Getuid()); ok != (err == nil) || ok != a.Capabilities().Session || ok != (a.session == testSession) {
92				t.Fatalf("err=%v caps=%+v session=%q", err, a.Capabilities(), a.session)
93			}
94			want := ""
95			if uid == uint32(os.Getuid()) {
96				want = "XDG_SESSION_ID"
97			}
98			if _, source := a.Session(); source != want {
99				t.Fatalf("source %q, want %q", source, want)
100			}
101		})
102	}
103}
104
105// A terminal started by the compositor can inherit a stale XDG_SESSION_ID and
106// run outside any session scope. An unknown, non-graphical or closing XDG
107// session falls through to the real UID's live graphical Display session.
108func TestProbeFallsBackToDisplaySession(t *testing.T) {
109	cases := map[string]struct {
110		xdg       string
111		xdgExists bool
112		typ       string
113		state     string
114	}{
115		"no XDG_SESSION_ID":      {},
116		"unknown XDG_SESSION_ID": {xdg: "c9"},
117		"tty XDG_SESSION_ID":     {xdg: "c9", xdgExists: true, typ: "tty"},
118		"closing XDG_SESSION_ID": {xdg: "c9", xdgExists: true, typ: "wayland", state: "closing"},
119	}
120	for name, tc := range cases {
121		t.Run(name, func(t *testing.T) {
122			t.Setenv("XDG_SESSION_ID", tc.xdg)
123			b := newMockbus(t)
124			expectNoPIDSession(b)
125			switch {
126			case tc.xdgExists:
127				b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".GetSession", []any{tc.xdg}).Return([]any{staleSession}, nil).Once()
128				expectSessionKind(b, staleSession, tc.typ, tc.state)
129			case tc.xdg != "":
130				b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".GetSession", []any{tc.xdg}).Return(nil, errXDG).Once()
131			}
132			expectDisplay(b, []any{"c1", testSession})
133			expectSessionKind(b, testSession, "wayland", "active")
134			expectSessionUID(b, testSession, uint32(os.Getuid()))
135			a := newAdapter(b, ports.SystemRequirements{Session: true})
136			if err := a.Probe(context.Background()); err != nil || a.session != testSession || !a.Capabilities().Session {
137				t.Fatalf("err=%v session=%q", err, a.session)
138			}
139			if _, source := a.Session(); source != "user display" {
140				t.Fatalf("source %q", source)
141			}
142		})
143	}
144}
145
146func TestProbeRejectsDisplaySessionOfOtherUID(t *testing.T) {
147	t.Setenv("XDG_SESSION_ID", "")
148	b := newMockbus(t)
149	expectNoPIDSession(b)
150	expectDisplay(b, []any{"c1", testSession})
151	expectSessionKind(b, testSession, "wayland", "active")
152	expectSessionUID(b, testSession, uint32(os.Getuid())+1)
153	a := newAdapter(b, ports.SystemRequirements{Session: true})
154	if err := a.Probe(context.Background()); err == nil || a.Capabilities().Session {
155		t.Fatal("other UID admitted through the Display fallback")
156	}
157}
158
159// Every source's cause stays inspectable through the joined error.
160func TestProbeWithoutAnySessionReportsEveryCause(t *testing.T) {
161	t.Setenv("XDG_SESSION_ID", "c9")
162	b := newMockbus(t)
163	expectNoPIDSession(b)
164	b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".GetSession", []any{"c9"}).Return(nil, errXDG).Once()
165	b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".GetUser", []any{uint32(os.Getuid())}).Return(nil, errUser).Once()
166	err := newAdapter(b, ports.SystemRequirements{Session: true}).Probe(context.Background())
167	for _, cause := range []error{errPID, errXDG, errUser} {
168		if !errors.Is(err, cause) {
169			t.Fatalf("err=%v lacks %v", err, cause)
170		}
171	}
172}
173
174func TestDisplaySessionRejectsMalformedReplies(t *testing.T) {
175	cases := map[string]struct {
176		user    []any
177		display any
178	}{
179		"empty user reply":   {user: []any{}},
180		"non-path user":      {user: []any{"_1000"}},
181		"display not tuple":  {user: []any{testUser}, display: "c1"},
182		"display short":      {user: []any{testUser}, display: []any{"c1"}},
183		"display not a path": {user: []any{testUser}, display: []any{"c1", "c1"}},
184		"no display session": {user: []any{testUser}, display: []any{"", dbus.ObjectPath("/")}},
185	}
186	for name, tc := range cases {
187		t.Run(name, func(t *testing.T) {
188			b := newMockbus(t)
189			b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".GetUser", []any{uint32(os.Getuid())}).Return(tc.user, nil).Once()
190			if tc.display != nil {
191				b.EXPECT().call(mock.Anything, testOwner, testUser, "org.freedesktop.DBus.Properties.Get", []any{userInterface, "Display"}).Return([]any{dbus.MakeVariant(tc.display)}, nil).Once()
192			}
193			a := newAdapter(b, ports.SystemRequirements{Session: true})
194			a.owner = testOwner
195			if path, err := a.displaySession(context.Background()); err == nil {
196				t.Fatalf("accepted %q", path)
197			}
198		})
199	}
200}
201
202func TestProbeRejectsOtherUID(t *testing.T) {
203	b := newMockbus(t)
204	expectProbe(t, b, uint32(os.Getuid())+1)
205	if err := newAdapter(b, ports.SystemRequirements{Session: true}).Probe(context.Background()); err == nil {
206		t.Fatal("other UID admitted")
207	}
208}
209func TestSignalFilteringAndUnlockNotAuthority(t *testing.T) {
210	cases := []struct {
211		sig   *dbus.Signal
212		valid bool
213	}{
214		{&dbus.Signal{Sender: testOwner, Path: testSession, Name: sessionInterface + ".Lock"}, true},
215		{&dbus.Signal{Sender: testOwner, Path: testSession, Name: sessionInterface + ".Unlock"}, true},
216		{&dbus.Signal{Sender: testOwner, Path: managerPath, Name: managerInterface + ".PrepareForSleep", Body: []any{true}}, true},
217		{&dbus.Signal{Sender: ":1.99", Path: testSession, Name: sessionInterface + ".Lock"}, false},
218		{&dbus.Signal{Sender: testOwner, Path: "/org/freedesktop/login1/session/other", Name: sessionInterface + ".Unlock"}, false},
219		{&dbus.Signal{Sender: testOwner, Path: managerPath, Name: managerInterface + ".PrepareForSleep", Body: []any{"true"}}, false},
220		{&dbus.Signal{Sender: testOwner, Path: testSession, Name: sessionInterface + ".Lock", Body: []any{true}}, false},
221	}
222	for _, tc := range cases {
223		event, ok := parseSignal(tc.sig, testOwner, testSession)
224		if ok != tc.valid {
225			t.Fatalf("signal %+v accepted=%v", tc.sig, ok)
226		}
227		if tc.sig.Name == sessionInterface+".Unlock" && ok {
228			if _, ok := event.(ports.SystemUnlockObserved); !ok {
229				t.Fatalf("unlock converted to authority %T", event)
230			}
231		}
232	}
233}
234func TestInhibitorOwnsCloexecDuplicateAndClosesOnce(t *testing.T) {
235	b := newMockbus(t)
236	r, w, err := os.Pipe()
237	if err != nil {
238		t.Fatal(err)
239	}
240	defer r.Close()
241	defer w.Close()
242	received, err := unix.Dup(int(r.Fd()))
243	if err != nil {
244		t.Fatal(err)
245	}
246	b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".Inhibit", []any{"sleep", "neferafk", "session protection", "delay"}).Return([]any{dbus.UnixFD(received)}, nil).Once()
247	b.EXPECT().close().Return(nil).Once()
248	a := newAdapter(b, ports.SystemRequirements{Sleep: true})
249	a.owner = testOwner
250	if err := a.inhibit(context.Background()); err != nil {
251		t.Fatal(err)
252	}
253	if _, err := unix.FcntlInt(uintptr(received), unix.F_GETFD, 0); !errors.Is(err, unix.EBADF) {
254		t.Fatalf("received FD not closed: %v", err)
255	}
256	held := a.inhibitor
257	flags, err := unix.FcntlInt(held.Fd(), unix.F_GETFD, 0)
258	if err != nil || flags&unix.FD_CLOEXEC == 0 {
259		t.Fatal("inhibitor not CLOEXEC")
260	}
261	a.release()
262	a.release()
263	if _, err := held.Stat(); !errors.Is(err, os.ErrClosed) {
264		t.Fatal("owned inhibitor still open")
265	}
266	if err := a.Close(); err != nil {
267		t.Fatal(err)
268	}
269	if err := a.Close(); err != nil {
270		t.Fatal(err)
271	}
272}
273func TestRunMatchingSleepCycleAndSuspendFalse(t *testing.T) {
274	b := newMockbus(t)
275	a := newAdapter(b, ports.SystemRequirements{Session: true, Sleep: true})
276	a.owner = testOwner
277	a.session = testSession
278	a.caps = ports.SystemCapabilities{Session: true, Suspend: true, DelayMax: 5 * time.Second}
279	b.EXPECT().signals(mock.Anything).Return().Once()
280	b.EXPECT().removeSignals(mock.Anything).Return().Once()
281	b.EXPECT().match(mock.Anything, mock.Anything).Return(nil).Twice()
282	b.EXPECT().unixFDs().Return(true).Once()
283	b.EXPECT().close().Return(nil).Once()
284	r, w, err := os.Pipe()
285	if err != nil {
286		t.Fatal(err)
287	}
288	defer r.Close()
289	defer w.Close()
290	received, err := unix.Dup(int(r.Fd()))
291	if err != nil {
292		t.Fatal(err)
293	}
294	b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".Inhibit", []any{"sleep", "neferafk", "session protection", "delay"}).Return([]any{dbus.UnixFD(received)}, nil).Once()
295	suspended := make(chan struct{})
296	b.EXPECT().call(mock.Anything, testOwner, managerPath, "org.freedesktop.DBus.Properties.Get", []any{managerInterface, "BlockInhibited"}).Return([]any{dbus.MakeVariant("")}, nil).Once()
297	b.EXPECT().suspend(mock.Anything, testOwner).RunAndReturn(func(ctx context.Context, _ string) <-chan *dbus.Call {
298		if _, ok := ctx.Deadline(); !ok {
299			t.Error("unbounded suspend call")
300		}
301		close(suspended)
302		return make(chan *dbus.Call, 1) // reply held: owner must still process SleepReady
303	}).Once()
304	commands := make(chan ports.SystemCommand)
305	events := make(chan ports.SystemEvent, 8)
306	done := make(chan error, 1)
307	go func() { done <- a.Run(context.Background(), commands, events) }()
308	<-events // capability report synchronizes initial inhibitor ownership
309	a.signals <- &dbus.Signal{Sender: testOwner, Path: managerPath, Name: managerInterface + ".PrepareForSleep", Body: []any{true}}
310	prep := (<-events).(ports.SleepPreparation)
311	if prep.Cycle != 1 || prep.MaxDelay != 5*time.Second || prep.ReceivedAt.IsZero() {
312		t.Fatalf("sleep prep %+v", prep)
313	}
314	commands <- ports.SystemSleepReady{Cycle: 99}
315	commands <- ports.SystemSuspend{}
316	<-suspended // ordered owner checkpoint, no live suspend involved
317	held := a.inhibitor
318	if held == nil {
319		t.Fatal("stale cycle released inhibitor")
320	}
321	commands <- ports.SystemSleepReady{Cycle: prep.Cycle}
322	close(commands)
323	select {
324	case err := <-done:
325		if err != nil {
326			t.Fatal(err)
327		}
328	case <-time.After(time.Second):
329		t.Fatal("Run blocked")
330	}
331	if _, err := held.Stat(); !errors.Is(err, os.ErrClosed) {
332		t.Fatal("matching ready did not close inhibitor")
333	}
334}
335func TestOpenOffDoesNotConnect(t *testing.T) {
336	if _, err := Open(context.Background(), ports.SystemRequirements{}); err == nil {
337		t.Fatal("disabled adapter unexpectedly opened")
338	}
339}
340
341func TestSuspendBlockedAndUnavailableNeverCallsSuspend(t *testing.T) {
342	for _, blocked := range []bool{true, false} {
343		t.Run(fmt.Sprint(blocked), func(t *testing.T) {
344			b := newMockbus(t)
345			a := newAdapter(b, ports.SystemRequirements{Sleep: true})
346			a.owner = testOwner
347			a.caps.Suspend = blocked
348			b.EXPECT().signals(mock.Anything).Return().Once()
349			b.EXPECT().removeSignals(mock.Anything).Return().Once()
350			b.EXPECT().match(mock.Anything, mock.Anything).Return(nil).Once()
351			b.EXPECT().unixFDs().Return(false).Once()
352			b.EXPECT().close().Return(nil).Once()
353			if blocked {
354				b.EXPECT().call(mock.Anything, testOwner, managerPath, "org.freedesktop.DBus.Properties.Get", []any{managerInterface, "BlockInhibited"}).Return([]any{dbus.MakeVariant("sleep:shutdown")}, nil).Once()
355			}
356			commands := make(chan ports.SystemCommand, 1)
357			events := make(chan ports.SystemEvent, 3)
358			commands <- ports.SystemSuspend{}
359			close(commands)
360			if err := a.Run(context.Background(), commands, events); err != nil {
361				t.Fatal(err)
362			}
363			<-events
364			if failure, ok := (<-events).(ports.SystemFailure); !ok || failure.Operation != "suspend" {
365				t.Fatal("missing unsupported/block failure")
366			}
367		})
368	}
369}

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