main f2d166b

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, 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}

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 26

Tags 10

↑↓ moveenter openesc close