1package uidriver
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "io"
8 "net"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/bnema/vev/internal/ports"
14 portsmocks "github.com/bnema/vev/internal/ports/mocks"
15 "github.com/stretchr/testify/mock"
16 "github.com/stretchr/testify/require"
17)
18
19func testSnapshot() ports.UISnapshot {
20 return ports.UISnapshot{Revision: 1, Columns: 1, Rows: 1, Context: ports.UIContext{AttachmentHandle: "a", Generation: 1, Status: ports.UIStatusAttached}, Cells: []ports.UICell{{Text: "x", Width: 1}}}
21}
22
23func testReady(control bool) Ready {
24 return Ready{Attachment: "a", Generation: 1, Control: control, Status: ports.UIStatusAttached}
25}
26
27func readEnvelope(t *testing.T, decoder *json.Decoder) envelope {
28 t.Helper()
29 var response envelope
30 require.NoError(t, decoder.Decode(&response))
31 return response
32}
33
34func TestServeCaptureProceedsWhileWaitIsPending(t *testing.T) {
35 service := portsmocks.NewMockUIService(t)
36 started := make(chan struct{})
37 release := make(chan struct{})
38 service.EXPECT().Wait(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, _ ports.UIWaitRequest) (ports.UIWaitResult, error) {
39 close(started)
40 select {
41 case <-release:
42 return ports.UIWaitResult{Revision: 2}, nil
43 case <-ctx.Done():
44 return ports.UIWaitResult{}, ctx.Err()
45 }
46 }).Once()
47 service.EXPECT().Capture("a").Return(testSnapshot(), nil).Once()
48 server := New(service, nil)
49 host, peer := net.Pipe()
50 defer func() { _ = peer.Close() }()
51 require.NoError(t, peer.SetDeadline(time.Now().Add(2*time.Second)))
52 done := make(chan error, 1)
53 go func() { done <- server.Serve(context.Background(), host, testReady(true)) }()
54 decoder := json.NewDecoder(peer)
55 ready := readEnvelope(t, decoder)
56 require.Zero(t, ready.ID)
57 _, err := io.WriteString(peer, "{\"version\":1,\"id\":1,\"op\":\"wait\",\"attachment\":\"a\",\"expect\":{\"status\":\"attached\"}}\n")
58 require.NoError(t, err)
59 select {
60 case <-started:
61 case <-time.After(time.Second):
62 t.Fatal("wait not started")
63 }
64 for range 2 {
65 _, err = io.WriteString(peer, "{\"version\":1,\"id\":1,\"op\":\"capture\",\"attachment\":\"a\"}\n")
66 require.NoError(t, err)
67 duplicate := readEnvelope(t, decoder)
68 require.Equal(t, ports.UIErrInvalidRequest, duplicate.Error.Code)
69 }
70 _, err = io.WriteString(peer, "{\"version\":1,\"id\":2,\"op\":\"capture\",\"attachment\":\"a\"}\n")
71 require.NoError(t, err)
72 capture := readEnvelope(t, decoder)
73 require.Equal(t, uint64(2), capture.ID)
74 require.Nil(t, capture.Error)
75 close(release)
76 waited := readEnvelope(t, decoder)
77 require.Equal(t, uint64(1), waited.ID)
78 require.Nil(t, waited.Error)
79 require.NoError(t, peer.Close())
80 select {
81 case err := <-done:
82 require.NoError(t, err)
83 case <-time.After(time.Second):
84 t.Fatal("connection did not stop")
85 }
86 require.Zero(t, server.serialized)
87}
88
89func TestServeReadOnlyRejectsBeforeServiceAction(t *testing.T) {
90 service := portsmocks.NewMockUIService(t)
91 host, peer := net.Pipe()
92 defer func() { _ = peer.Close() }()
93 require.NoError(t, peer.SetDeadline(time.Now().Add(2*time.Second)))
94 done := make(chan error, 1)
95 go func() { done <- New(service, nil).Serve(context.Background(), host, testReady(false)) }()
96 decoder := json.NewDecoder(peer)
97 readEnvelope(t, decoder)
98 _, err := io.WriteString(peer, "{\"version\":1,\"id\":1,\"op\":\"text\",\"attachment\":\"a\",\"generation\":1,\"text\":\"x\"}\n")
99 require.NoError(t, err)
100 denied := readEnvelope(t, decoder)
101 require.Equal(t, ports.UIErrPermissionDenied, denied.Error.Code)
102 require.False(t, denied.Error.Accepted)
103 require.NoError(t, peer.Close())
104 select {
105 case <-done:
106 case <-time.After(time.Second):
107 t.Fatal("connection did not stop")
108 }
109 service.AssertNotCalled(t, "Action", mock.Anything, mock.Anything)
110}
111
112type finiteStream struct {
113 input *strings.Reader
114 bytes.Buffer
115}
116
117func (s *finiteStream) Read(data []byte) (int, error) { return s.input.Read(data) }
118func (*finiteStream) Close() error { return nil }
119
120func TestServeAcceptsFinalObjectWithoutNewline(t *testing.T) {
121 service := portsmocks.NewMockUIService(t)
122 service.EXPECT().Capture("a").Return(testSnapshot(), nil).Once()
123 stream := &finiteStream{input: strings.NewReader(`{"version":1,"id":1,"op":"capture","attachment":"a"}`)}
124 server := New(service, nil)
125 require.NoError(t, server.Serve(context.Background(), stream, testReady(true)))
126 decoder := json.NewDecoder(bytes.NewReader(stream.Bytes()))
127 require.Zero(t, readEnvelope(t, decoder).ID)
128 require.Equal(t, uint64(1), readEnvelope(t, decoder).ID)
129 require.Zero(t, server.serialized)
130}
131
132func TestServeWriteDeadlineClosesNonreadingStream(t *testing.T) {
133 clock := portsmocks.NewMockClock(t)
134 timer := portsmocks.NewMockTimer(t)
135 expired := make(chan time.Time, 1)
136 created := make(chan struct{})
137 clock.EXPECT().NewTimer(writeTimeout).RunAndReturn(func(time.Duration) ports.Timer { close(created); return timer }).Once()
138 timer.EXPECT().C().Return(expired).Once()
139 timer.EXPECT().Stop().Return(true).Once()
140 server := New(nil, clock)
141 host, peer := net.Pipe()
142 defer func() { _ = peer.Close() }()
143 done := make(chan error, 1)
144 go func() { done <- server.Serve(context.Background(), host, testReady(false)) }()
145 select {
146 case <-created:
147 case <-time.After(time.Second):
148 t.Fatal("writer did not arm deadline")
149 }
150 expired <- time.Time{}
151 select {
152 case err := <-done:
153 require.Error(t, err)
154 case <-time.After(time.Second):
155 t.Fatal("nonreading observer retained workers")
156 }
157 require.Zero(t, server.serialized)
158 require.Zero(t, server.connections)
159}
160
161func TestSerializedBudgetKeepsSpaceForBusyErrors(t *testing.T) {
162 server := New(nil, nil)
163 for range 2 {
164 require.True(t, server.reserve(maxResponseBytes))
165 }
166 require.False(t, server.reserve(maxResponseBytes))
167 require.True(t, server.reserve(errorResponseReserve))
168 require.LessOrEqual(t, server.serialized, maxSerializedBytes)
169}