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.ch.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.lockSurfaces) != 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.ch.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.ch.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}