1
+ import { describe , it , expect , vi , beforeEach , afterEach } from "vitest"
2
+ import * as vscode from "vscode"
3
+ import { Inbox } from "./inbox"
4
+ import { Api } from "coder/site/src/api/api"
5
+ import { Workspace } from "coder/site/src/api/typesGenerated"
6
+ import { ProxyAgent } from "proxy-agent"
7
+ import { WebSocket } from "ws"
8
+ import { Storage } from "./storage"
9
+
10
+ // Mock external dependencies
11
+ vi . mock ( "vscode" , ( ) => ( {
12
+ window : {
13
+ showInformationMessage : vi . fn ( ) ,
14
+ } ,
15
+ } ) )
16
+
17
+ vi . mock ( "ws" , ( ) => ( {
18
+ WebSocket : vi . fn ( ) ,
19
+ } ) )
20
+
21
+ vi . mock ( "proxy-agent" , ( ) => ( {
22
+ ProxyAgent : vi . fn ( ) ,
23
+ } ) )
24
+
25
+ vi . mock ( "./api" , ( ) => ( {
26
+ coderSessionTokenHeader : "Coder-Session-Token" ,
27
+ } ) )
28
+
29
+ vi . mock ( "./api-helper" , ( ) => ( {
30
+ errToStr : vi . fn ( ) ,
31
+ } ) )
32
+
33
+ describe ( "Inbox" , ( ) => {
34
+ let mockWorkspace : Workspace
35
+ let mockHttpAgent : ProxyAgent
36
+ let mockRestClient : Api
37
+ let mockStorage : Storage
38
+ let mockSocket : any
39
+ let inbox : Inbox
40
+
41
+ beforeEach ( async ( ) => {
42
+ vi . clearAllMocks ( )
43
+
44
+ // Setup mock workspace
45
+ mockWorkspace = {
46
+ id : "workspace-1" ,
47
+ name : "test-workspace" ,
48
+ owner_name : "testuser" ,
49
+ } as Workspace
50
+
51
+ // Setup mock HTTP agent
52
+ mockHttpAgent = { } as ProxyAgent
53
+
54
+ // Setup mock socket
55
+ mockSocket = {
56
+ on : vi . fn ( ) ,
57
+ close : vi . fn ( ) ,
58
+ }
59
+ vi . mocked ( WebSocket ) . mockReturnValue ( mockSocket )
60
+
61
+ // Setup mock REST client
62
+ mockRestClient = {
63
+ getAxiosInstance : vi . fn ( ( ) => ( {
64
+ defaults : {
65
+ baseURL : "https://coder.example.com" ,
66
+ headers : {
67
+ common : {
68
+ "Coder-Session-Token" : "test-token" ,
69
+ } ,
70
+ } ,
71
+ } ,
72
+ } ) ) ,
73
+ } as any
74
+
75
+ // Setup mock storage
76
+ mockStorage = {
77
+ writeToCoderOutputChannel : vi . fn ( ) ,
78
+ } as any
79
+
80
+ // Setup errToStr mock
81
+ const apiHelper = await import ( "./api-helper" )
82
+ vi . mocked ( apiHelper . errToStr ) . mockReturnValue ( "Mock error message" )
83
+ } )
84
+
85
+ afterEach ( ( ) => {
86
+ if ( inbox ) {
87
+ inbox . dispose ( )
88
+ }
89
+ } )
90
+
91
+ describe ( "constructor" , ( ) => {
92
+ it ( "should create WebSocket connection with correct URL and headers" , ( ) => {
93
+ inbox = new Inbox ( mockWorkspace , mockHttpAgent , mockRestClient , mockStorage )
94
+
95
+ expect ( WebSocket ) . toHaveBeenCalledWith (
96
+ expect . any ( URL ) ,
97
+ {
98
+ agent : mockHttpAgent ,
99
+ followRedirects : true ,
100
+ headers : {
101
+ "Coder-Session-Token" : "test-token" ,
102
+ } ,
103
+ }
104
+ )
105
+
106
+ // Verify the WebSocket URL is constructed correctly
107
+ const websocketCall = vi . mocked ( WebSocket ) . mock . calls [ 0 ]
108
+ const websocketUrl = websocketCall [ 0 ] as URL
109
+ expect ( websocketUrl . protocol ) . toBe ( "wss:" )
110
+ expect ( websocketUrl . host ) . toBe ( "coder.example.com" )
111
+ expect ( websocketUrl . pathname ) . toBe ( "/api/v2/notifications/inbox/watch" )
112
+ expect ( websocketUrl . searchParams . get ( "format" ) ) . toBe ( "plaintext" )
113
+ expect ( websocketUrl . searchParams . get ( "templates" ) ) . toContain ( "a9d027b4-ac49-4fb1-9f6d-45af15f64e7a" )
114
+ expect ( websocketUrl . searchParams . get ( "templates" ) ) . toContain ( "f047f6a3-5713-40f7-85aa-0394cce9fa3a" )
115
+ expect ( websocketUrl . searchParams . get ( "targets" ) ) . toBe ( "workspace-1" )
116
+ } )
117
+
118
+ it ( "should use ws protocol for http base URL" , ( ) => {
119
+ mockRestClient . getAxiosInstance = vi . fn ( ( ) => ( {
120
+ defaults : {
121
+ baseURL : "http://coder.example.com" ,
122
+ headers : {
123
+ common : {
124
+ "Coder-Session-Token" : "test-token" ,
125
+ } ,
126
+ } ,
127
+ } ,
128
+ } ) )
129
+
130
+ inbox = new Inbox ( mockWorkspace , mockHttpAgent , mockRestClient , mockStorage )
131
+
132
+ const websocketCall = vi . mocked ( WebSocket ) . mock . calls [ 0 ]
133
+ const websocketUrl = websocketCall [ 0 ] as URL
134
+ expect ( websocketUrl . protocol ) . toBe ( "ws:" )
135
+ } )
136
+
137
+ it ( "should handle missing token in headers" , ( ) => {
138
+ mockRestClient . getAxiosInstance = vi . fn ( ( ) => ( {
139
+ defaults : {
140
+ baseURL : "https://coder.example.com" ,
141
+ headers : {
142
+ common : { } ,
143
+ } ,
144
+ } ,
145
+ } ) )
146
+
147
+ inbox = new Inbox ( mockWorkspace , mockHttpAgent , mockRestClient , mockStorage )
148
+
149
+ expect ( WebSocket ) . toHaveBeenCalledWith (
150
+ expect . any ( URL ) ,
151
+ {
152
+ agent : mockHttpAgent ,
153
+ followRedirects : true ,
154
+ headers : undefined ,
155
+ }
156
+ )
157
+ } )
158
+
159
+ it ( "should throw error when no base URL is set" , ( ) => {
160
+ mockRestClient . getAxiosInstance = vi . fn ( ( ) => ( {
161
+ defaults : {
162
+ baseURL : undefined ,
163
+ headers : {
164
+ common : { } ,
165
+ } ,
166
+ } ,
167
+ } ) )
168
+
169
+ expect ( ( ) => {
170
+ new Inbox ( mockWorkspace , mockHttpAgent , mockRestClient , mockStorage )
171
+ } ) . toThrow ( "No base URL set on REST client" )
172
+ } )
173
+
174
+ it ( "should register socket event handlers" , ( ) => {
175
+ inbox = new Inbox ( mockWorkspace , mockHttpAgent , mockRestClient , mockStorage )
176
+
177
+ expect ( mockSocket . on ) . toHaveBeenCalledWith ( "open" , expect . any ( Function ) )
178
+ expect ( mockSocket . on ) . toHaveBeenCalledWith ( "error" , expect . any ( Function ) )
179
+ expect ( mockSocket . on ) . toHaveBeenCalledWith ( "message" , expect . any ( Function ) )
180
+ } )
181
+ } )
182
+
183
+ describe ( "socket event handlers" , ( ) => {
184
+ beforeEach ( ( ) => {
185
+ inbox = new Inbox ( mockWorkspace , mockHttpAgent , mockRestClient , mockStorage )
186
+ } )
187
+
188
+ it ( "should handle socket open event" , ( ) => {
189
+ const openHandler = mockSocket . on . mock . calls . find ( call => call [ 0 ] === "open" ) ?. [ 1 ]
190
+ expect ( openHandler ) . toBeDefined ( )
191
+
192
+ openHandler ( )
193
+
194
+ expect ( mockStorage . writeToCoderOutputChannel ) . toHaveBeenCalledWith (
195
+ "Listening to Coder Inbox"
196
+ )
197
+ } )
198
+
199
+ it ( "should handle socket error event" , ( ) => {
200
+ const errorHandler = mockSocket . on . mock . calls . find ( call => call [ 0 ] === "error" ) ?. [ 1 ]
201
+ expect ( errorHandler ) . toBeDefined ( )
202
+
203
+ const mockError = new Error ( "Socket error" )
204
+ const disposeSpy = vi . spyOn ( inbox , "dispose" )
205
+
206
+ errorHandler ( mockError )
207
+
208
+ expect ( mockStorage . writeToCoderOutputChannel ) . toHaveBeenCalledWith ( "Mock error message" )
209
+ expect ( disposeSpy ) . toHaveBeenCalled ( )
210
+ } )
211
+
212
+ it ( "should handle valid socket message" , ( ) => {
213
+ const messageHandler = mockSocket . on . mock . calls . find ( call => call [ 0 ] === "message" ) ?. [ 1 ]
214
+ expect ( messageHandler ) . toBeDefined ( )
215
+
216
+ const mockMessage = {
217
+ notification : {
218
+ title : "Test notification" ,
219
+ } ,
220
+ }
221
+ const messageData = Buffer . from ( JSON . stringify ( mockMessage ) )
222
+
223
+ messageHandler ( messageData )
224
+
225
+ expect ( vscode . window . showInformationMessage ) . toHaveBeenCalledWith ( "Test notification" )
226
+ } )
227
+
228
+ it ( "should handle invalid JSON in socket message" , ( ) => {
229
+ const messageHandler = mockSocket . on . mock . calls . find ( call => call [ 0 ] === "message" ) ?. [ 1 ]
230
+ expect ( messageHandler ) . toBeDefined ( )
231
+
232
+ const invalidData = Buffer . from ( "invalid json" )
233
+
234
+ messageHandler ( invalidData )
235
+
236
+ expect ( mockStorage . writeToCoderOutputChannel ) . toHaveBeenCalledWith ( "Mock error message" )
237
+ } )
238
+
239
+ it ( "should handle message parsing errors" , ( ) => {
240
+ const messageHandler = mockSocket . on . mock . calls . find ( call => call [ 0 ] === "message" ) ?. [ 1 ]
241
+ expect ( messageHandler ) . toBeDefined ( )
242
+
243
+ const mockMessage = {
244
+ // Missing required notification structure
245
+ }
246
+ const messageData = Buffer . from ( JSON . stringify ( mockMessage ) )
247
+
248
+ messageHandler ( messageData )
249
+
250
+ // Should not throw, but may not show notification if structure is wrong
251
+ // The test verifies that error handling doesn't crash the application
252
+ } )
253
+ } )
254
+
255
+ describe ( "dispose" , ( ) => {
256
+ beforeEach ( ( ) => {
257
+ inbox = new Inbox ( mockWorkspace , mockHttpAgent , mockRestClient , mockStorage )
258
+ } )
259
+
260
+ it ( "should close socket and log when disposed" , ( ) => {
261
+ inbox . dispose ( )
262
+
263
+ expect ( mockStorage . writeToCoderOutputChannel ) . toHaveBeenCalledWith (
264
+ "No longer listening to Coder Inbox"
265
+ )
266
+ expect ( mockSocket . close ) . toHaveBeenCalled ( )
267
+ } )
268
+
269
+ it ( "should handle multiple dispose calls safely" , ( ) => {
270
+ inbox . dispose ( )
271
+ inbox . dispose ( )
272
+
273
+ // Should only log and close once
274
+ expect ( mockStorage . writeToCoderOutputChannel ) . toHaveBeenCalledTimes ( 1 )
275
+ expect ( mockSocket . close ) . toHaveBeenCalledTimes ( 1 )
276
+ } )
277
+ } )
278
+
279
+ describe ( "template constants" , ( ) => {
280
+ it ( "should include workspace out of memory template" , ( ) => {
281
+ inbox = new Inbox ( mockWorkspace , mockHttpAgent , mockRestClient , mockStorage )
282
+
283
+ const websocketCall = vi . mocked ( WebSocket ) . mock . calls [ 0 ]
284
+ const websocketUrl = websocketCall [ 0 ] as URL
285
+ const templates = websocketUrl . searchParams . get ( "templates" )
286
+
287
+ expect ( templates ) . toContain ( "a9d027b4-ac49-4fb1-9f6d-45af15f64e7a" )
288
+ } )
289
+
290
+ it ( "should include workspace out of disk template" , ( ) => {
291
+ inbox = new Inbox ( mockWorkspace , mockHttpAgent , mockRestClient , mockStorage )
292
+
293
+ const websocketCall = vi . mocked ( WebSocket ) . mock . calls [ 0 ]
294
+ const websocketUrl = websocketCall [ 0 ] as URL
295
+ const templates = websocketUrl . searchParams . get ( "templates" )
296
+
297
+ expect ( templates ) . toContain ( "f047f6a3-5713-40f7-85aa-0394cce9fa3a" )
298
+ } )
299
+ } )
300
+ } )
0 commit comments