1package browser
2
3import (
4 "encoding/json"
5 "strings"
6 "testing"
7
8 "github.com/stretchr/testify/require"
9)
10
11func TestDecodeEventValidatesTheClosedSchema(t *testing.T) {
12 event, err := DecodeEvent([]byte(`{"schemaVersion":2,"type":"text","text":"é"}`), EventLimits{})
13 require.NoError(t, err)
14 require.Equal(t, EventText, event.Kind)
15 require.Equal(t, "é", event.Text.Text)
16
17 _, err = DecodeEvent([]byte(`{"schemaVersion":2,"type":"text","text":"x","unknown":true}`), EventLimits{})
18 require.Error(t, err)
19
20 _, err = DecodeEvent([]byte(`{"schemaVersion":2,"type":"paste","text":"toolong"}`), EventLimits{MaxPasteBytes: 3})
21 require.ErrorIs(t, err, ErrEventLimit)
22
23 _, err = DecodeEvent([]byte{'{', '"', 'x', '"', ':', '"', 0xff, '"', '}'}, EventLimits{})
24 require.ErrorContains(t, err, "not valid UTF-8")
25
26 _, err = DecodeEvent([]byte(`{"schemaVersion":2,"type":"resize","columns":80,"rows":24,"pixelWidth":800,"pixelHeight":480,"cellWidth":10,"cellHeight":20,"devicePixelRatio":1}`), EventLimits{})
27 require.NoError(t, err)
28
29 for _, data := range []string{
30 `{"schemaVersion":2,"type":"focus"}`,
31 `{"schemaVersion":2,"type":"wheel"}`,
32 } {
33 _, err = DecodeEvent([]byte(data), EventLimits{})
34 require.Error(t, err)
35 }
36}
37
38func TestDecodeEventAcceptsRuntimeEventKinds(t *testing.T) {
39 tests := []struct {
40 kind EventKind
41 json string
42 }{
43 {EventKey, `{"schemaVersion":2,"type":"key","key":"ArrowUp","code":"ArrowUp","alt":false,"ctrl":false,"meta":false,"shift":false,"repeat":false,"location":0}`},
44 {EventPaste, `{"schemaVersion":2,"type":"paste","text":"hello"}`},
45 {EventPointer, `{"schemaVersion":2,"type":"pointer","action":"down","button":0,"buttons":1,"row":0,"column":0,"x":1,"y":2,"alt":false,"ctrl":false,"meta":false,"shift":false}`},
46 {EventWheel, `{"schemaVersion":2,"type":"wheel","deltaX":1,"deltaY":2,"deltaMode":0,"row":0,"column":0,"alt":false,"ctrl":false,"meta":false,"shift":false}`},
47 {EventFocus, `{"schemaVersion":2,"type":"focus","focused":true}`},
48 }
49 for _, test := range tests {
50 t.Run(string(test.kind), func(t *testing.T) {
51 event, err := DecodeEvent([]byte(test.json), EventLimits{})
52 require.NoError(t, err)
53 require.Equal(t, test.kind, event.Kind)
54 if test.kind == EventKey {
55 require.Equal(t, "ArrowUp", event.Key.Key)
56 }
57 })
58 }
59}
60
61func TestDefaultEventLimitsAcceptEscapedMaximumPaste(t *testing.T) {
62 limits := DefaultEventLimits()
63 text := strings.Repeat("\x00", limits.MaxPasteBytes)
64 data, err := json.Marshal(textWire{
65 SchemaVersion: EventSchemaVersion,
66 Kind: EventPaste,
67 Text: &text,
68 })
69 require.NoError(t, err)
70
71 event, err := DecodeEvent(data, EventLimits{})
72 require.NoError(t, err)
73 require.NotNil(t, event.Paste)
74 require.Len(t, event.Paste.Text, limits.MaxPasteBytes)
75}
76
77func TestNormalizeEventLimitsRejectsImpossibleEnvelope(t *testing.T) {
78 _, err := normalizeEventLimits(EventLimits{MaxEventBytes: 69, MaxTextBytes: 1, MaxPasteBytes: 1})
79 require.ErrorIs(t, err, ErrInvalidEventLimits)
80
81 _, err = normalizeEventLimits(EventLimits{MaxTextBytes: -1})
82 require.ErrorIs(t, err, ErrInvalidEventLimits)
83}
84
85func TestDecodeEventRejectsMissingRequiredFields(t *testing.T) {
86 tests := []string{
87 `{"schemaVersion":2,"type":"text"}`,
88 `{"schemaVersion":2,"type":"key","code":"ArrowUp"}`,
89 `{"schemaVersion":2,"type":"pointer","action":"down"}`,
90 `{"schemaVersion":2,"type":"wheel"}`,
91 `{"schemaVersion":2,"type":"resize","columns":80,"rows":24}`,
92 `{"schemaVersion":2,"type":"focus"}`,
93 }
94 for _, data := range tests {
95 _, err := DecodeEvent([]byte(data), EventLimits{})
96 require.Error(t, err, data)
97 }
98}
99
100func TestDecodeEventRejectsInvalidRuntimePayloads(t *testing.T) {
101 tests := map[string]string{
102 "schema": `{"schemaVersion":3,"type":"focus","focused":true}`,
103 "pointer button": `{"schemaVersion":2,"type":"pointer","action":"down","button":5,"buttons":1,"row":0,"column":0,"x":1,"y":2}`,
104 "pointer buttons": `{"schemaVersion":2,"type":"pointer","action":"down","button":0,"buttons":32,"row":0,"column":0,"x":1,"y":2}`,
105 "wheel mode": `{"schemaVersion":2,"type":"wheel","deltaX":1,"deltaY":2,"deltaMode":3,"row":0,"column":0}`,
106 "resize dimension": `{"schemaVersion":2,"type":"resize","columns":0,"rows":24,"pixelWidth":800,"pixelHeight":480,"cellWidth":10,"cellHeight":20,"devicePixelRatio":1}`,
107 "non-finite coordinate": `{"schemaVersion":2,"type":"pointer","action":"move","button":-1,"buttons":0,"row":0,"column":0,"x":1e999,"y":2}`,
108 "trailing value": `{"schemaVersion":2,"type":"focus","focused":true} {}`,
109 }
110 for name, data := range tests {
111 t.Run(name, func(t *testing.T) {
112 _, err := DecodeEvent([]byte(data), EventLimits{})
113 require.Error(t, err)
114 })
115 }
116}
117
118func FuzzDecodeEvent(f *testing.F) {
119 f.Add([]byte(`{"schemaVersion":2,"type":"focus","focused":true}`))
120 f.Add([]byte(`{"schemaVersion":2,"type":"text","text":"hello"}`))
121 f.Add([]byte(`not json`))
122 f.Add([]byte(`{"schemaVersion":2,"type":"text","text":"` + strings.Repeat("x", 4<<10) + `"}`))
123 f.Fuzz(func(t *testing.T, data []byte) {
124 event, err := DecodeEvent(data, EventLimits{MaxEventBytes: 4 << 10, MaxTextBytes: 512, MaxPasteBytes: 512})
125 if len(data) > 4<<10 {
126 require.ErrorIs(t, err, ErrEventLimit)
127 return
128 }
129 if err != nil {
130 return
131 }
132 payloads := map[EventKind]bool{
133 EventText: event.Text != nil,
134 EventKey: event.Key != nil,
135 EventPaste: event.Paste != nil,
136 EventPointer: event.Pointer != nil,
137 EventWheel: event.Wheel != nil,
138 EventResize: event.Resize != nil,
139 EventFocus: event.Focus != nil,
140 }
141 set := 0
142 for _, present := range payloads {
143 if present {
144 set++
145 }
146 }
147 if set != 1 || !payloads[event.Kind] {
148 t.Fatalf("decoded event violates the closed union: %#v", event)
149 }
150 })
151}