feat/data-dnd e05cb15

security_internal_test.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"testing"
6	"time"
7
8	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
9	"github.com/bnema/neferwl/internal/ports"
10)
11
12func TestProtectedDropsPrivateOffscreenCaptureAndDesktopEffects(t *testing.T) {
13	c, ws, commands := hiddenCaptureCommands(t)
14	state := ports.SecurityState{Generation: 1, Protected: true}
15	gate := portsmocks.NewMockSessionSecurity(t)
16	gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state })
17	c.opts.Security = gate
18	c.captureOpen(ports.CaptureSessionOpen{ID: 7, Workspace: ws.ID})
19	c.captureExclusionBegin(ports.CaptureExclusionBegin{Session: 7})
20	c.capt.flashes = []capFlash{{target: capTarget{output: "A"}, until: c.now().Add(time.Hour)}}
21	c.screens[0].layers = []ports.LayerSurface{{ID: 50, Layer: ports.LayerOverlay, Keyboard: 1}}
22	c.pressed["password"] = true
23	c.buttons[0x110] = true
24	c.pointer, c.grab = 1, 1
25	c.keyboard = keyboard{layer: 50, sent: 1, inhibiting: 1}
26	c.constrained = ports.PointerConstrained{ID: 1, Mode: ports.ConstraintLock}
27	c.swipe = &swipeGesture{}
28	if err := c.publish(context.Background()); err != nil {
29		t.Fatal(err)
30	}
31	scenes := <-c.ch.Scenes
32	s := scenes[0]
33	if s.Security != state || s.Capture != nil || s.CaptureScene != nil || len(s.CaptureIndicators) != 0 || len(s.Windows) != 0 || len(s.Layers) != 0 || len(s.Separators) != 0 || s.Background != "#000000" {
34		t.Fatalf("unsafe protected scene %+v", s)
35	}
36	if len(c.capt.sessions) != 0 || c.capt.excl != nil || len(c.capt.flashes) != 0 || c.configures.cw.active() || len(c.pressed) != 0 || len(c.buttons) != 0 || c.pointer != 0 || c.grab != 0 || c.keyboard.layer != 0 || c.constrained.ID != 0 || c.swipe != nil {
37		t.Fatal("transition kept desktop input/capture state")
38	}
39	// Logical dimensions must match exactly, including after output changes.
40	c.applyLockChanged(ports.SessionLockChanged{State: state, Surfaces: []ports.LockSurfacePlacement{{ID: 10, Output: "A", Width: 299, Height: 200}}})
41	if c.lockKeyboardFocus() != 0 {
42		t.Fatal("wrong dimensions accepted")
43	}
44	c.applyLockChanged(ports.SessionLockChanged{State: state, Surfaces: []ports.LockSurfacePlacement{{ID: 10, Output: "A", Width: 300, Height: 200}}})
45	if c.lockKeyboardFocus() != 10 {
46		t.Fatal("mapped role not focused")
47	}
48	for _, ev := range []ports.ClientEvent{ports.WindowActivate{ID: 1}, ports.WorkspaceActivate{}, ports.PointerWarp{ID: 1}, ports.PointerConstrained{ID: 1}, ports.PopupRequest{}, ports.CaptureSessionOpen{}, ports.CaptureFrameTaken{}, ports.CaptureExclusionBegin{}, ports.CaptureExclusionLayer{}} {
49		if !blockedProtectedEvent(ev) {
50			t.Fatalf("allowed desktop effect %T", ev)
51		}
52	}
53	for len(commands) > 0 {
54		if _, ok := (<-commands).(ports.SecurityCommand); !ok {
55			t.Fatal("unwrapped protected command")
56		}
57	}
58	// Release is a transition too: password presses, button grabs and
59	// keyboard ownership cannot survive it, even with freshly stamped releases.
60	c.inputKeys["#30"] = true
61	c.buttons[0x110] = true
62	c.pointer, c.grab = 10, 10
63	c.keyboard = keyboard{sent: 10}
64	state = ports.SecurityState{Generation: 2}
65	if !c.syncSecurity() || len(c.inputKeys) != 0 || c.grab != 0 || c.keyboard.sent != 0 || len(c.lock.surfaces) != 0 {
66		t.Fatal("release did not reset input ownership")
67	}
68	for _, ev := range []ports.InputEvent{ports.KeyEvent{Keycode: 30}, ports.PointerButton{Button: 0x110}} {
69		if _, ok := c.admitInput(ports.SecurityInput{State: state, Event: ev}); ok {
70			t.Fatalf("release crossed epoch: %T", ev)
71		}
72	}
73}
74
75// Lock clients map their surfaces one by one: the focused output's surface
76// takes keyboard focus when it maps after another one, unless the user
77// already clicked or typed into a surface.
78func TestLockFocusMovesToFocusedOutputSurfaceUntilPinned(t *testing.T) {
79	for _, tc := range []struct {
80		name  string
81		input []ports.InputEvent // on A, before B maps
82		want  WindowID
83	}{
84		{"no input", nil, 11},
85		{"click on focused A", []ports.InputEvent{ports.PointerMotion{X: 10, Y: 10}, ports.PointerButton{Button: 0x110, Pressed: true}}, 10},
86		{"typing", []ports.InputEvent{ports.KeyEvent{Keysym: "a", Keycode: 30, Pressed: true}}, 10},
87	} {
88		t.Run(tc.name, func(t *testing.T) {
89			c, _, _ := hiddenCaptureCommands(t)
90			c.addScreen(ports.OutputInfo{Name: "B", Width: 400, Height: 300})
91			c.focusScreen = c.screenIndex("B")
92			state := ports.SecurityState{Generation: 1, Protected: true}
93			gate := portsmocks.NewMockSessionSecurity(t)
94			gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state })
95			c.opts.Security = gate
96			c.syncSecurity()
97			ctx := context.Background()
98			mapped := func(s ...ports.LockSurfacePlacement) {
99				t.Helper()
100				c.applyLockChanged(ports.SessionLockChanged{State: state, Surfaces: s})
101				if err := c.publishProtected(ctx); err != nil {
102					t.Fatal(err)
103				}
104			}
105
106			a := ports.LockSurfacePlacement{ID: 10, Output: "A", Width: 300, Height: 200}
107			b := ports.LockSurfacePlacement{ID: 11, Output: "B", Width: 400, Height: 300}
108			mapped(a)
109			if c.keyboard.sent != 10 {
110				t.Fatalf("only mapped surface not focused: %d", c.keyboard.sent)
111			}
112			for _, ev := range tc.input {
113				if err := c.protectedInput(ctx, ev); err != nil {
114					t.Fatal(err)
115				}
116			}
117			mapped(a, b)
118			if c.keyboard.sent != tc.want {
119				t.Fatalf("keyboard focus %d, want %d", c.keyboard.sent, tc.want)
120			}
121		})
122	}
123}
124
125func TestWiredZeroEpochRejectsRawInputAndCommandKeepsOwnerEpoch(t *testing.T) {
126	c, _, commands := hiddenCaptureCommands(t)
127	gate := portsmocks.NewMockSessionSecurity(t)
128	next := ports.SecurityState{Generation: 1, Protected: true}
129	gate.EXPECT().Snapshot().Return(next)
130	c.opts.Security = gate
131	if _, ok := c.admitInput(ports.KeyEvent{Pressed: true}); ok {
132		t.Fatal("raw wired input admitted")
133	}
134	if _, ok := c.admitInput(ports.SecurityInput{Event: ports.KeyEvent{Pressed: true}}); !ok {
135		t.Fatal("zero epoch wrapped input rejected")
136	}
137	// Gate changes after selection must not relabel an old command as new.
138	if err := c.command(context.Background(), ports.ForwardKey{ID: 1}); err != nil {
139		t.Fatal(err)
140	}
141	v := (<-commands).(ports.SecurityCommand)
142	if v.State != (ports.SecurityState{}) {
143		t.Fatalf("relabeled stale command: %+v", v)
144	}
145	c.syncSecurity()
146	if _, ok := c.admitInput(ports.SecurityInput{Event: ports.KeyEvent{Pressed: true}}); ok {
147		t.Fatal("stale input admitted")
148	}
149}
150
151// A fullscreen request dropped while locked is still answered with a
152// configure once unlocked (xdg-shell).
153func TestProtectedFullscreenRequestAnswered(t *testing.T) {
154	c, _, _ := hiddenCaptureCommands(t)
155	p := Placement{ID: 1, Rect: Rect{W: 10, H: 10}}
156	v, _ := c.configures.next(p, configureTarget{})
157	c.configures.mark(v)
158	c.security = ports.SecurityState{Generation: 1, Protected: true}
159	if !c.blockProtected(ports.WindowFullscreenRequest{ID: 1, Fullscreen: true}) {
160		t.Fatal("fullscreen request allowed while locked")
161	}
162	c.security = ports.SecurityState{Generation: 2}
163	if _, send := c.configures.next(p, configureTarget{}); !send {
164		t.Fatal("locked request not answered")
165	}
166}

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