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