security_test.go RawHistoryFolder

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}

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 25

Tags 10

↑↓ moveenter openesc close