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