v0.1.0 07cdc18

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	// No mutating call was configured: generated mock rejects Inhibit/Suspend.
41}
42func TestProbeRejectsOtherUID(t *testing.T) {
43	b := newMockbus(t)
44	expectProbe(t, b, uint32(os.Getuid())+1)
45	if err := newAdapter(b, ports.SystemRequirements{Session: true}).Probe(context.Background()); err == nil {
46		t.Fatal("other UID admitted")
47	}
48}
49func TestSignalFilteringAndUnlockNotAuthority(t *testing.T) {
50	cases := []struct {
51		sig   *dbus.Signal
52		valid bool
53	}{
54		{&dbus.Signal{Sender: testOwner, Path: testSession, Name: sessionInterface + ".Lock"}, true},
55		{&dbus.Signal{Sender: testOwner, Path: testSession, Name: sessionInterface + ".Unlock"}, true},
56		{&dbus.Signal{Sender: testOwner, Path: managerPath, Name: managerInterface + ".PrepareForSleep", Body: []any{true}}, true},
57		{&dbus.Signal{Sender: ":1.99", Path: testSession, Name: sessionInterface + ".Lock"}, false},
58		{&dbus.Signal{Sender: testOwner, Path: "/org/freedesktop/login1/session/other", Name: sessionInterface + ".Unlock"}, false},
59		{&dbus.Signal{Sender: testOwner, Path: managerPath, Name: managerInterface + ".PrepareForSleep", Body: []any{"true"}}, false},
60		{&dbus.Signal{Sender: testOwner, Path: testSession, Name: sessionInterface + ".Lock", Body: []any{true}}, false},
61	}
62	for _, tc := range cases {
63		event, ok := parseSignal(tc.sig, testOwner, testSession)
64		if ok != tc.valid {
65			t.Fatalf("signal %+v accepted=%v", tc.sig, ok)
66		}
67		if tc.sig.Name == sessionInterface+".Unlock" && ok {
68			if _, ok := event.(ports.SystemUnlockObserved); !ok {
69				t.Fatalf("unlock converted to authority %T", event)
70			}
71		}
72	}
73}
74func TestInhibitorOwnsCloexecDuplicateAndClosesOnce(t *testing.T) {
75	b := newMockbus(t)
76	r, w, err := os.Pipe()
77	if err != nil {
78		t.Fatal(err)
79	}
80	defer r.Close()
81	defer w.Close()
82	received, err := unix.Dup(int(r.Fd()))
83	if err != nil {
84		t.Fatal(err)
85	}
86	b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".Inhibit", []any{"sleep", "neferafk", "session protection", "delay"}).Return([]any{dbus.UnixFD(received)}, nil).Once()
87	b.EXPECT().close().Return(nil).Once()
88	a := newAdapter(b, ports.SystemRequirements{Sleep: true})
89	a.owner = testOwner
90	if err := a.inhibit(context.Background()); err != nil {
91		t.Fatal(err)
92	}
93	if _, err := unix.FcntlInt(uintptr(received), unix.F_GETFD, 0); !errors.Is(err, unix.EBADF) {
94		t.Fatalf("received FD not closed: %v", err)
95	}
96	held := a.inhibitor
97	flags, err := unix.FcntlInt(held.Fd(), unix.F_GETFD, 0)
98	if err != nil || flags&unix.FD_CLOEXEC == 0 {
99		t.Fatal("inhibitor not CLOEXEC")
100	}
101	a.release()
102	a.release()
103	if _, err := held.Stat(); !errors.Is(err, os.ErrClosed) {
104		t.Fatal("owned inhibitor still open")
105	}
106	if err := a.Close(); err != nil {
107		t.Fatal(err)
108	}
109	if err := a.Close(); err != nil {
110		t.Fatal(err)
111	}
112}
113func TestRunMatchingSleepCycleAndSuspendFalse(t *testing.T) {
114	b := newMockbus(t)
115	a := newAdapter(b, ports.SystemRequirements{Session: true, Sleep: true})
116	a.owner = testOwner
117	a.session = testSession
118	a.caps = ports.SystemCapabilities{Session: true, Suspend: true, DelayMax: 5 * time.Second}
119	b.EXPECT().signals(mock.Anything).Return().Once()
120	b.EXPECT().removeSignals(mock.Anything).Return().Once()
121	b.EXPECT().match(mock.Anything, mock.Anything).Return(nil).Twice()
122	b.EXPECT().unixFDs().Return(true).Once()
123	b.EXPECT().close().Return(nil).Once()
124	r, w, err := os.Pipe()
125	if err != nil {
126		t.Fatal(err)
127	}
128	defer r.Close()
129	defer w.Close()
130	received, err := unix.Dup(int(r.Fd()))
131	if err != nil {
132		t.Fatal(err)
133	}
134	b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".Inhibit", []any{"sleep", "neferafk", "session protection", "delay"}).Return([]any{dbus.UnixFD(received)}, nil).Once()
135	suspended := make(chan struct{})
136	b.EXPECT().call(mock.Anything, testOwner, managerPath, "org.freedesktop.DBus.Properties.Get", []any{managerInterface, "BlockInhibited"}).Return([]any{dbus.MakeVariant("")}, nil).Once()
137	b.EXPECT().suspend(mock.Anything, testOwner).RunAndReturn(func(ctx context.Context, _ string) <-chan *dbus.Call {
138		if _, ok := ctx.Deadline(); !ok {
139			t.Error("unbounded suspend call")
140		}
141		close(suspended)
142		return make(chan *dbus.Call, 1) // reply held: owner must still process SleepReady
143	}).Once()
144	commands := make(chan ports.SystemCommand)
145	events := make(chan ports.SystemEvent, 8)
146	done := make(chan error, 1)
147	go func() { done <- a.Run(context.Background(), commands, events) }()
148	<-events // capability report synchronizes initial inhibitor ownership
149	a.signals <- &dbus.Signal{Sender: testOwner, Path: managerPath, Name: managerInterface + ".PrepareForSleep", Body: []any{true}}
150	prep := (<-events).(ports.SleepPreparation)
151	if prep.Cycle != 1 || prep.MaxDelay != 5*time.Second || prep.ReceivedAt.IsZero() {
152		t.Fatalf("sleep prep %+v", prep)
153	}
154	commands <- ports.SystemSleepReady{Cycle: 99}
155	commands <- ports.SystemSuspend{}
156	<-suspended // ordered owner checkpoint, no live suspend involved
157	held := a.inhibitor
158	if held == nil {
159		t.Fatal("stale cycle released inhibitor")
160	}
161	commands <- ports.SystemSleepReady{Cycle: prep.Cycle}
162	close(commands)
163	select {
164	case err := <-done:
165		if err != nil {
166			t.Fatal(err)
167		}
168	case <-time.After(time.Second):
169		t.Fatal("Run blocked")
170	}
171	if _, err := held.Stat(); !errors.Is(err, os.ErrClosed) {
172		t.Fatal("matching ready did not close inhibitor")
173	}
174}
175func TestOpenOffDoesNotConnect(t *testing.T) {
176	if _, err := Open(context.Background(), ports.SystemRequirements{}); err == nil {
177		t.Fatal("disabled adapter unexpectedly opened")
178	}
179}
180
181func TestSuspendBlockedAndUnavailableNeverCallsSuspend(t *testing.T) {
182	for _, blocked := range []bool{true, false} {
183		t.Run(fmt.Sprint(blocked), func(t *testing.T) {
184			b := newMockbus(t)
185			a := newAdapter(b, ports.SystemRequirements{Sleep: true})
186			a.owner = testOwner
187			a.caps.Suspend = blocked
188			b.EXPECT().signals(mock.Anything).Return().Once()
189			b.EXPECT().removeSignals(mock.Anything).Return().Once()
190			b.EXPECT().match(mock.Anything, mock.Anything).Return(nil).Once()
191			b.EXPECT().unixFDs().Return(false).Once()
192			b.EXPECT().close().Return(nil).Once()
193			if blocked {
194				b.EXPECT().call(mock.Anything, testOwner, managerPath, "org.freedesktop.DBus.Properties.Get", []any{managerInterface, "BlockInhibited"}).Return([]any{dbus.MakeVariant("sleep:shutdown")}, nil).Once()
195			}
196			commands := make(chan ports.SystemCommand, 1)
197			events := make(chan ports.SystemEvent, 3)
198			commands <- ports.SystemSuspend{}
199			close(commands)
200			if err := a.Run(context.Background(), commands, events); err != nil {
201				t.Fatal(err)
202			}
203			<-events
204			if failure, ok := (<-events).(ports.SystemFailure); !ok || failure.Operation != "suspend" {
205				t.Fatal("missing unsupported/block failure")
206			}
207		})
208	}
209}

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