Skip to content

Commit 1afefc5

Browse files
jaggederestclaude
andcommitted
test: add comprehensive tests for inbox.ts and workspaceMonitor.ts
- Add 14 test cases for inbox.ts covering WebSocket connection, event handling, and disposal - Add 19 test cases for workspaceMonitor.ts covering SSE monitoring, notifications, and status bar updates - Test WebSocket setup with proper URL construction and authentication headers - Test EventSource setup for workspace monitoring with data/error event handling - Test notification logic for autostop, deletion, outdated workspace, and non-running states - Test status bar updates and context management - Test proper cleanup and disposal patterns - Achieve comprehensive coverage for message handling and workspace monitoring functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 01246a1 commit 1afefc5

File tree

2 files changed

+773
-0
lines changed

2 files changed

+773
-0
lines changed

src/inbox.test.ts

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
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

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy