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}