1package app
2
3import (
4 "context"
5 "errors"
6 "os"
7 "testing"
8 "time"
9
10 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
11 "github.com/bnema/neferwl/internal/ports"
12)
13
14func TestSecurityRegistrationBeforeStart(t *testing.T) {
15 ctx, cancel := context.WithCancel(context.Background())
16 defer cancel()
17 events := make(chan ports.SecurityBackendEvent)
18 set := newOutputSet(ctx, nil)
19 set.wireSecurity(outputChannels{securityEvents: events})
20 set.setSecurity(ports.SecurityState{Generation: 7, Protected: true})
21 started := make(chan ports.OutputInstance, 1)
22 result := make(chan bool, 1)
23 go func() {
24 result <- set.start(ctx, "DP-1", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error {
25 state := <-security
26 if state != (ports.SecurityState{Generation: 7, Protected: true}) {
27 return errors.New("missing startup protection")
28 }
29 started <- instance
30 <-ctx.Done()
31 return nil
32 })
33 }()
34 select {
35 case <-started:
36 t.Fatal("owner ran before registration")
37 default:
38 }
39 var added ports.SecurityOutputAdded
40 select {
41 case event := <-events:
42 added = event.(ports.SecurityOutputAdded)
43 case <-time.After(2 * time.Second):
44 t.Fatal("registration missing")
45 }
46 if added.Instance == 0 || added.Output != "DP-1" {
47 t.Fatalf("registration: %+v", added)
48 }
49 if !<-result {
50 t.Fatal("start failed")
51 }
52 select {
53 case instance := <-started:
54 if instance != added.Instance {
55 t.Fatal("owner lifetime differs")
56 }
57 case <-time.After(2 * time.Second):
58 t.Fatal("owner missing startup protection")
59 }
60 if err := set.wait(); err != nil {
61 t.Fatal(err)
62 }
63}
64
65func TestSecurityReplacementAndStaleState(t *testing.T) {
66 ctx := context.Background()
67 events := make(chan ports.SecurityBackendEvent, 4)
68 set := newOutputSet(ctx, nil)
69 set.wireSecurity(outputChannels{securityEvents: events})
70 run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error {
71 <-ctx.Done()
72 return nil
73 }
74 set.start(ctx, "DP-1", run)
75 old := (<-events).(ports.SecurityOutputAdded)
76 set.outs["DP-1"].stop()
77 if err := set.finish("DP-1"); err != nil {
78 t.Fatal(err)
79 }
80 set.start(ctx, "DP-1", run)
81 replacement := (<-events).(ports.SecurityOutputAdded)
82 if replacement.Instance <= old.Instance {
83 t.Fatal("reused lifetime")
84 }
85 if set.stoppedCurrent(outputStopped{name: "DP-1", instance: old.Instance}) {
86 t.Fatal("old stop matches replacement")
87 }
88 set.setSecurity(ports.SecurityState{Generation: 3, Protected: true})
89 set.setSecurity(ports.SecurityState{Generation: 2})
90 if state := <-set.outs["DP-1"].security; state.Generation != 3 || !state.Protected {
91 t.Fatalf("stale transition: %+v", state)
92 }
93 set.setSecurity(ports.SecurityState{Generation: 4})
94 if state := <-set.outs["DP-1"].security; state.Generation != 4 || state.Protected {
95 t.Fatalf("unlock not forwarded: %+v", state)
96 }
97 if err := set.wait(); err != nil {
98 t.Fatal(err)
99 }
100 select {
101 case ev := <-events:
102 t.Fatalf("stop emitted false removal: %+v", ev)
103 default:
104 }
105}
106
107func TestGuardedLeaseRejectsProtected(t *testing.T) {
108 gate := portsmocks.NewMockSessionSecurity(t)
109 gate.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 1, Protected: true}).Once()
110 card := newMocksecurityLeaseCard(t)
111 fd, id, err := guardedLease(card, gate, []string{"DP-1"})
112 if fd != nil || id != 0 || err == nil {
113 t.Fatalf("protected lease: %v %d %v", fd, id, err)
114 }
115}
116
117func TestGuardedLeaseCrossingTransition(t *testing.T) {
118 for _, revokeErr := range []error{nil, errors.New("revoke failed")} {
119 t.Run(map[bool]string{false: "revoked", true: "failed"}[revokeErr != nil], func(t *testing.T) {
120 fd, err := os.CreateTemp(t.TempDir(), "lease")
121 if err != nil {
122 t.Fatal(err)
123 }
124 gate := portsmocks.NewMockSessionSecurity(t)
125 gate.EXPECT().Snapshot().Return(ports.SecurityState{}).Once()
126 gate.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 1, Protected: true}).Once()
127 card := newMocksecurityLeaseCard(t)
128 card.EXPECT().Lease([]string{"DP-1"}).Return(fd, uint32(12), nil).Once()
129 card.EXPECT().Revoke(uint32(12)).Return(revokeErr).Once()
130 got, id, err := guardedLease(card, gate, []string{"DP-1"})
131 if got != nil || id != 0 || err == nil {
132 t.Fatalf("late grant escaped: %v %d %v", got, id, err)
133 }
134 if _, statErr := fd.Stat(); !errors.Is(statErr, os.ErrClosed) {
135 t.Fatalf("FD not closed: %v", statErr)
136 }
137 if revokeErr != nil && !errors.Is(err, revokeErr) {
138 t.Fatal("revoke failure lost")
139 }
140 })
141 }
142}
143
144func TestSecurityRevokesAllLeasesAndRetainsFailures(t *testing.T) {
145 card := newMocksecurityLeaseCard(t)
146 card.EXPECT().LeaseIDs().Return([]uint32{1, 2}).Once()
147 card.EXPECT().LeaseIDs().Return([]uint32{2}).Once()
148 card.EXPECT().Revoke(uint32(1)).Return(nil).Once()
149 failed := errors.New("native revoke failure")
150 card.EXPECT().Revoke(uint32(2)).Return(failed).Once()
151 card.EXPECT().Path().Return("card0")
152 var finished []uint32
153 err := revokeSecurityLeases(card, func(id uint32) { finished = append(finished, id) })
154 if !errors.Is(err, failed) || len(finished) != 1 || finished[0] != 1 {
155 t.Fatalf("revocation: %v finished %v", err, finished)
156 }
157 card.EXPECT().LeaseIDs().Return([]uint32{2}).Once()
158 card.EXPECT().LeaseIDs().Return([]uint32{}).Once()
159 card.EXPECT().Revoke(uint32(2)).Return(nil).Once()
160 if err := revokeSecurityLeases(card, func(id uint32) {}); err != nil {
161 t.Fatal(err)
162 }
163}
164
165func TestSecurityInstanceExhaustion(t *testing.T) {
166 set := newOutputSet(context.Background(), nil)
167 events := make(chan ports.SecurityBackendEvent, 2)
168 set.wireSecurity(outputChannels{securityEvents: events})
169 set.nextInstance = ^ports.OutputInstance(0) - 1
170 run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error {
171 <-ctx.Done()
172 return nil
173 }
174 if !set.start(context.Background(), "last", run) {
175 t.Fatal("last valid lifetime refused")
176 }
177 if added := (<-events).(ports.SecurityOutputAdded); added.Instance != ^ports.OutputInstance(0) {
178 t.Fatalf("last instance: %+v", added)
179 }
180 for range 2 {
181 if set.start(context.Background(), "overflow", run) {
182 t.Fatal("exhausted counter launched owner")
183 }
184 }
185 if set.nextInstance != ^ports.OutputInstance(0) || set.startErr == nil {
186 t.Fatal("counter wrapped or error missing")
187 }
188 select {
189 case ev := <-events:
190 t.Fatalf("registered overflow: %+v", ev)
191 default:
192 }
193 if err := set.wait(); err == nil {
194 t.Fatal("exhaustion not returned")
195 }
196}
197
198// A saturated shared display event channel must not pin the backend outside
199// its select: a lease barrier can finish before the channel is drained.
200func TestSecurityFullEventsAllowLeaseBarrier(t *testing.T) {
201 ctx, cancel := context.WithCancel(context.Background())
202 defer cancel()
203 set := newOutputSet(ctx, nil)
204 events := make(chan ports.SecurityBackendEvent, 1)
205 set.wireSecurity(outputChannels{securityEvents: events})
206 var ownerEvents chan<- ports.SecurityBackendEvent
207 set.start(ctx, "DP-1", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan *ports.ContentBatch, _ ports.ContentReturn, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error {
208 <-ctx.Done()
209 return nil
210 }, &ownerEvents)
211 <-events // ordered registration
212 events <- ports.SecurityBackendBarrier{Generation: 99} // blocked display
213 r := set.outs["DP-1"]
214 first := ports.SecurityOutputInvalidated{Generation: 1, Instance: r.instance}
215 second := ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: 1, Instance: r.instance, Kind: ports.ProtectionProtectedFrame}}
216 ownerEvents <- first
217 ownerEvents <- second
218 card := newMocksecurityLeaseCard(t)
219 card.EXPECT().LeaseIDs().Return([]uint32{9}).Once()
220 card.EXPECT().LeaseIDs().Return([]uint32{}).Once()
221 card.EXPECT().Revoke(uint32(9)).Return(nil).Once()
222 progressed := make(chan error, 1)
223 go func() {
224 set.setSecurity(ports.SecurityState{Generation: 1, Protected: true})
225 err := revokeSecurityLeases(card, func(uint32) {})
226 set.securityEvent(ports.SecurityBackendBarrier{Generation: 1, Err: err})
227 progressed <- err
228 }()
229 select {
230 case err := <-progressed:
231 if err != nil {
232 t.Fatal(err)
233 }
234 case <-time.After(time.Second):
235 t.Fatal("event backpressure blocked lease transition")
236 }
237 <-events // unblock display
238 var ownerSequence []ports.SecurityBackendEvent
239 for range 3 {
240 select {
241 case ev := <-events:
242 switch ev.(type) {
243 case ports.SecurityOutputInvalidated, ports.SecurityOutputProof:
244 ownerSequence = append(ownerSequence, ev)
245 }
246 case <-time.After(time.Second):
247 t.Fatal("lossless forwarder lost event")
248 }
249 }
250 if len(ownerSequence) != 2 || ownerSequence[0] != first || ownerSequence[1] != second {
251 t.Fatalf("owner order lost: %+v", ownerSequence)
252 }
253 if err := set.wait(); err != nil {
254 t.Fatal(err)
255 }
256}