blob: c6d4b6b021c0d8241797d42b1b1e001c86bd9118 [file] [log] [blame]
Philip Zeyliger25f6ff12025-05-02 04:24:10 +00001package server_test
2
3import (
4 "bufio"
5 "context"
6 "net/http"
7 "net/http/httptest"
8 "slices"
9 "strings"
10 "sync"
11 "testing"
12 "time"
13
14 "sketch.dev/llm/conversation"
15 "sketch.dev/loop"
16 "sketch.dev/loop/server"
Philip Zeyliger5f26a342025-07-04 01:30:29 +000017 "tailscale.com/portlist"
Philip Zeyliger25f6ff12025-05-02 04:24:10 +000018)
19
20// mockAgent is a mock implementation of loop.CodingAgent for testing
21type mockAgent struct {
Philip Zeyligereab12de2025-05-14 02:35:53 +000022 mu sync.RWMutex
23 messages []loop.AgentMessage
24 messageCount int
25 currentState string
26 subscribers []chan *loop.AgentMessage
27 stateTransitionListeners []chan loop.StateTransition
bankseancad67b02025-06-27 21:57:05 +000028 gitUsername string
Philip Zeyligereab12de2025-05-14 02:35:53 +000029 initialCommit string
Philip Zeyligereab12de2025-05-14 02:35:53 +000030 branchName string
Philip Zeyligerbe7802a2025-06-04 20:15:25 +000031 branchPrefix string
Philip Zeyligerd3ac1122025-05-14 02:54:18 +000032 workingDir string
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070033 sessionID string
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -070034 slug string
35 retryNumber int
Philip Zeyliger0113be52025-06-07 23:53:41 +000036 skabandAddr string
Philip Zeyliger25f6ff12025-05-02 04:24:10 +000037}
38
banksean5ab8fb82025-07-09 12:34:55 -070039// TokenContextWindow implements loop.CodingAgent.
40func (m *mockAgent) TokenContextWindow() int {
41 return 200000
42}
43
Philip Zeyliger25f6ff12025-05-02 04:24:10 +000044func (m *mockAgent) NewIterator(ctx context.Context, nextMessageIdx int) loop.MessageIterator {
45 m.mu.RLock()
46 // Send existing messages that should be available immediately
47 ch := make(chan *loop.AgentMessage, 100)
48 iter := &mockIterator{
49 agent: m,
50 ctx: ctx,
51 nextMessageIdx: nextMessageIdx,
52 ch: ch,
53 }
54 m.mu.RUnlock()
55 return iter
56}
57
58type mockIterator struct {
59 agent *mockAgent
60 ctx context.Context
61 nextMessageIdx int
62 ch chan *loop.AgentMessage
63 subscribed bool
64}
65
66func (m *mockIterator) Next() *loop.AgentMessage {
67 if !m.subscribed {
68 m.agent.mu.Lock()
69 m.agent.subscribers = append(m.agent.subscribers, m.ch)
70 m.agent.mu.Unlock()
71 m.subscribed = true
72 }
73
74 for {
75 select {
76 case <-m.ctx.Done():
77 return nil
78 case msg := <-m.ch:
79 return msg
80 }
81 }
82}
83
84func (m *mockIterator) Close() {
85 // Remove from subscribers using slices.Delete
86 m.agent.mu.Lock()
87 for i, ch := range m.agent.subscribers {
88 if ch == m.ch {
89 m.agent.subscribers = slices.Delete(m.agent.subscribers, i, i+1)
90 break
91 }
92 }
93 m.agent.mu.Unlock()
94 close(m.ch)
95}
96
97func (m *mockAgent) Messages(start int, end int) []loop.AgentMessage {
98 m.mu.RLock()
99 defer m.mu.RUnlock()
100
101 if start >= len(m.messages) || end > len(m.messages) || start < 0 || end < 0 {
102 return []loop.AgentMessage{}
103 }
104 return slices.Clone(m.messages[start:end])
105}
106
107func (m *mockAgent) MessageCount() int {
108 m.mu.RLock()
109 defer m.mu.RUnlock()
110 return m.messageCount
111}
112
113func (m *mockAgent) AddMessage(msg loop.AgentMessage) {
114 m.mu.Lock()
115 msg.Idx = m.messageCount
116 m.messages = append(m.messages, msg)
117 m.messageCount++
118
119 // Create a copy of subscribers to avoid holding the lock while sending
120 subscribers := make([]chan *loop.AgentMessage, len(m.subscribers))
121 copy(subscribers, m.subscribers)
122 m.mu.Unlock()
123
124 // Notify subscribers
125 msgCopy := msg // Create a copy to avoid race conditions
126 for _, ch := range subscribers {
127 ch <- &msgCopy
128 }
129}
130
Philip Zeyligereab12de2025-05-14 02:35:53 +0000131func (m *mockAgent) NewStateTransitionIterator(ctx context.Context) loop.StateTransitionIterator {
132 m.mu.Lock()
133 ch := make(chan loop.StateTransition, 10)
134 m.stateTransitionListeners = append(m.stateTransitionListeners, ch)
135 m.mu.Unlock()
136
137 return &mockStateTransitionIterator{
138 agent: m,
139 ctx: ctx,
140 ch: ch,
141 }
142}
143
144type mockStateTransitionIterator struct {
145 agent *mockAgent
146 ctx context.Context
147 ch chan loop.StateTransition
148}
149
150func (m *mockStateTransitionIterator) Next() *loop.StateTransition {
151 select {
152 case <-m.ctx.Done():
153 return nil
154 case transition, ok := <-m.ch:
155 if !ok {
156 return nil
157 }
158 transitionCopy := transition
159 return &transitionCopy
160 }
161}
162
163func (m *mockStateTransitionIterator) Close() {
164 m.agent.mu.Lock()
165 for i, ch := range m.agent.stateTransitionListeners {
166 if ch == m.ch {
167 m.agent.stateTransitionListeners = slices.Delete(m.agent.stateTransitionListeners, i, i+1)
168 break
169 }
170 }
171 m.agent.mu.Unlock()
172 close(m.ch)
173}
174
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000175func (m *mockAgent) CurrentStateName() string {
176 m.mu.RLock()
177 defer m.mu.RUnlock()
178 return m.currentState
179}
180
Philip Zeyligereab12de2025-05-14 02:35:53 +0000181func (m *mockAgent) TriggerStateTransition(from, to loop.State, event loop.TransitionEvent) {
182 m.mu.Lock()
183 m.currentState = to.String()
184 transition := loop.StateTransition{
185 From: from,
186 To: to,
187 Event: event,
188 }
189
190 // Create a copy of listeners to avoid holding the lock while sending
191 listeners := make([]chan loop.StateTransition, len(m.stateTransitionListeners))
192 copy(listeners, m.stateTransitionListeners)
193 m.mu.Unlock()
194
195 // Notify listeners
196 for _, ch := range listeners {
197 ch <- transition
198 }
199}
200
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000201func (m *mockAgent) InitialCommit() string {
202 m.mu.RLock()
203 defer m.mu.RUnlock()
204 return m.initialCommit
205}
206
Philip Zeyliger49edc922025-05-14 09:45:45 -0700207func (m *mockAgent) SketchGitBase() string {
208 m.mu.RLock()
209 defer m.mu.RUnlock()
210 return m.initialCommit
211}
212
Philip Zeyligerd3ac1122025-05-14 02:54:18 +0000213func (m *mockAgent) SketchGitBaseRef() string {
214 return "sketch-base-test-session"
215}
216
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000217func (m *mockAgent) BranchName() string {
218 m.mu.RLock()
219 defer m.mu.RUnlock()
220 return m.branchName
221}
222
223// Other required methods of loop.CodingAgent with minimal implementation
224func (m *mockAgent) Init(loop.AgentInit) error { return nil }
225func (m *mockAgent) Ready() <-chan struct{} { ch := make(chan struct{}); close(ch); return ch }
226func (m *mockAgent) URL() string { return "http://localhost:8080" }
227func (m *mockAgent) UserMessage(ctx context.Context, msg string) {}
228func (m *mockAgent) Loop(ctx context.Context) {}
229func (m *mockAgent) CancelTurn(cause error) {}
230func (m *mockAgent) CancelToolUse(id string, cause error) error { return nil }
231func (m *mockAgent) TotalUsage() conversation.CumulativeUsage { return conversation.CumulativeUsage{} }
232func (m *mockAgent) OriginalBudget() conversation.Budget { return conversation.Budget{} }
Philip Zeyligerd3ac1122025-05-14 02:54:18 +0000233func (m *mockAgent) WorkingDir() string { return m.workingDir }
Josh Bleecher Snyderc5848f32025-05-28 18:50:58 +0000234func (m *mockAgent) RepoRoot() string { return m.workingDir }
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000235func (m *mockAgent) Diff(commit *string) (string, error) { return "", nil }
236func (m *mockAgent) OS() string { return "linux" }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700237func (m *mockAgent) SessionID() string { return m.sessionID }
philip.zeyliger8773e682025-06-11 21:36:21 -0700238func (m *mockAgent) SSHConnectionString() string { return "sketch-" + m.sessionID }
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000239func (m *mockAgent) BranchPrefix() string { return m.branchPrefix }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700240func (m *mockAgent) CurrentTodoContent() string { return "" } // Mock returns empty for simplicity
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000241func (m *mockAgent) OutstandingLLMCallCount() int { return 0 }
242func (m *mockAgent) OutstandingToolCalls() []string { return nil }
243func (m *mockAgent) OutsideOS() string { return "linux" }
244func (m *mockAgent) OutsideHostname() string { return "test-host" }
245func (m *mockAgent) OutsideWorkingDir() string { return "/app" }
246func (m *mockAgent) GitOrigin() string { return "" }
bankseancad67b02025-06-27 21:57:05 +0000247func (m *mockAgent) GitUsername() string { return m.gitUsername }
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000248func (m *mockAgent) OpenBrowser(url string) {}
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700249func (m *mockAgent) CompactConversation(ctx context.Context) error {
250 // Mock implementation - just return nil
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000251 return nil
252}
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700253func (m *mockAgent) IsInContainer() bool { return false }
254func (m *mockAgent) FirstMessageIndex() int { return 0 }
255func (m *mockAgent) DetectGitChanges(ctx context.Context) error { return nil }
Philip Zeyligerb5739402025-06-02 07:04:34 -0700256
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700257func (m *mockAgent) Slug() string {
258 m.mu.RLock()
259 defer m.mu.RUnlock()
260 return m.slug
261}
262
263func (m *mockAgent) IncrementRetryNumber() {
264 m.mu.Lock()
265 defer m.mu.Unlock()
266 m.retryNumber++
267}
268
Philip Zeyligerda623b52025-07-04 01:12:38 +0000269func (m *mockAgent) SkabandAddr() string { return m.skabandAddr }
270func (m *mockAgent) LinkToGitHub() bool { return false }
271func (m *mockAgent) DiffStats() (int, int) { return 0, 0 }
Philip Zeyliger5f26a342025-07-04 01:30:29 +0000272func (m *mockAgent) GetPorts() []portlist.Port {
273 // Mock returns a few test ports
274 return []portlist.Port{
275 {Proto: "tcp", Port: 22, Process: "sshd", Pid: 1234},
276 {Proto: "tcp", Port: 80, Process: "nginx", Pid: 5678},
277 {Proto: "tcp", Port: 8080, Process: "test-server", Pid: 9012},
278 }
279}
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700280
281// TestSSEStream tests the SSE stream endpoint
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000282func TestSSEStream(t *testing.T) {
283 // Create a mock agent with initial messages
284 mockAgent := &mockAgent{
Philip Zeyligereab12de2025-05-14 02:35:53 +0000285 messages: []loop.AgentMessage{},
286 messageCount: 0,
287 currentState: "Ready",
288 subscribers: []chan *loop.AgentMessage{},
289 stateTransitionListeners: []chan loop.StateTransition{},
290 initialCommit: "abcd1234",
Philip Zeyligereab12de2025-05-14 02:35:53 +0000291 branchName: "sketch/test-branch",
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000292 branchPrefix: "sketch/",
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700293 slug: "test-slug",
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000294 }
295
296 // Add the initial messages before creating the server
297 // to ensure they're available in the Messages slice
298 msg1 := loop.AgentMessage{
299 Type: loop.UserMessageType,
300 Content: "Hello, this is a test message",
301 Timestamp: time.Now(),
302 }
303 mockAgent.messages = append(mockAgent.messages, msg1)
304 msg1.Idx = mockAgent.messageCount
305 mockAgent.messageCount++
306
307 msg2 := loop.AgentMessage{
308 Type: loop.AgentMessageType,
309 Content: "This is a response message",
310 Timestamp: time.Now(),
311 EndOfTurn: true,
312 }
313 mockAgent.messages = append(mockAgent.messages, msg2)
314 msg2.Idx = mockAgent.messageCount
315 mockAgent.messageCount++
316
317 // Create a server with the mock agent
318 srv, err := server.New(mockAgent, nil)
319 if err != nil {
320 t.Fatalf("Failed to create server: %v", err)
321 }
322
323 // Create a test server
324 ts := httptest.NewServer(srv)
325 defer ts.Close()
326
327 // Create a context with cancellation for the client request
328 ctx, cancel := context.WithCancel(context.Background())
329
330 // Create a request to the /stream endpoint
331 req, err := http.NewRequestWithContext(ctx, "GET", ts.URL+"/stream?from=0", nil)
332 if err != nil {
333 t.Fatalf("Failed to create request: %v", err)
334 }
335
336 // Execute the request
337 res, err := http.DefaultClient.Do(req)
338 if err != nil {
339 t.Fatalf("Failed to execute request: %v", err)
340 }
341 defer res.Body.Close()
342
343 // Check response status
344 if res.StatusCode != http.StatusOK {
345 t.Fatalf("Expected status OK, got %v", res.Status)
346 }
347
348 // Check content type
349 if contentType := res.Header.Get("Content-Type"); contentType != "text/event-stream" {
350 t.Fatalf("Expected Content-Type text/event-stream, got %s", contentType)
351 }
352
353 // Read response events using a scanner
354 scanner := bufio.NewScanner(res.Body)
355
356 // Track events received
357 eventsReceived := map[string]int{
358 "state": 0,
359 "message": 0,
360 "heartbeat": 0,
361 }
362
363 // Read for a short time to capture initial state and messages
364 dataLines := []string{}
365 eventType := ""
366
367 go func() {
368 // After reading for a while, add a new message to test real-time updates
369 time.Sleep(500 * time.Millisecond)
370
371 mockAgent.AddMessage(loop.AgentMessage{
372 Type: loop.ToolUseMessageType,
373 Content: "This is a new real-time message",
374 Timestamp: time.Now(),
375 ToolName: "test_tool",
376 })
377
Philip Zeyligereab12de2025-05-14 02:35:53 +0000378 // Trigger a state transition to test state updates
379 time.Sleep(200 * time.Millisecond)
380 mockAgent.TriggerStateTransition(loop.StateReady, loop.StateSendingToLLM, loop.TransitionEvent{
381 Description: "Agent started thinking",
382 Data: "start_thinking",
383 })
384
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000385 // Let it process for longer
386 time.Sleep(1000 * time.Millisecond)
387 cancel() // Cancel to end the test
388 }()
389
390 // Read events
391 for scanner.Scan() {
392 line := scanner.Text()
393
394 if strings.HasPrefix(line, "event: ") {
395 eventType = strings.TrimPrefix(line, "event: ")
396 eventsReceived[eventType]++
397 } else if strings.HasPrefix(line, "data: ") {
398 dataLines = append(dataLines, line)
399 } else if line == "" && eventType != "" {
400 // End of event
401 eventType = ""
402 }
403
404 // Break if context is done
405 if ctx.Err() != nil {
406 break
407 }
408 }
409
410 if err := scanner.Err(); err != nil && ctx.Err() == nil {
411 t.Fatalf("Scanner error: %v", err)
412 }
413
414 // Simplified validation - just make sure we received something
415 t.Logf("Events received: %v", eventsReceived)
416 t.Logf("Data lines received: %d", len(dataLines))
417
418 // Basic validation that we received at least some events
419 if eventsReceived["state"] == 0 && eventsReceived["message"] == 0 {
420 t.Errorf("Did not receive any events")
421 }
422}
Philip Zeyligerd3ac1122025-05-14 02:54:18 +0000423
424func TestGitRawDiffHandler(t *testing.T) {
425 // Create a mock agent
426 mockAgent := &mockAgent{
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000427 workingDir: t.TempDir(), // Use a temp directory
428 branchPrefix: "sketch/",
Philip Zeyligerd3ac1122025-05-14 02:54:18 +0000429 }
430
431 // Create the server with the mock agent
432 server, err := server.New(mockAgent, nil)
433 if err != nil {
434 t.Fatalf("Failed to create server: %v", err)
435 }
436
437 // Create a test HTTP server
438 testServer := httptest.NewServer(server)
439 defer testServer.Close()
440
441 // Test missing parameters
442 resp, err := http.Get(testServer.URL + "/git/rawdiff")
443 if err != nil {
444 t.Fatalf("Failed to make HTTP request: %v", err)
445 }
446 if resp.StatusCode != http.StatusBadRequest {
447 t.Errorf("Expected status bad request, got: %d", resp.StatusCode)
448 }
449
450 // Test with commit parameter (this will fail due to no git repo, but we're testing the API, not git)
451 resp, err = http.Get(testServer.URL + "/git/rawdiff?commit=HEAD")
452 if err != nil {
453 t.Fatalf("Failed to make HTTP request: %v", err)
454 }
455 // We expect an error since there's no git repository, but the request should be processed
456 if resp.StatusCode != http.StatusInternalServerError {
457 t.Errorf("Expected status 500, got: %d", resp.StatusCode)
458 }
459
460 // Test with from/to parameters
461 resp, err = http.Get(testServer.URL + "/git/rawdiff?from=HEAD~1&to=HEAD")
462 if err != nil {
463 t.Fatalf("Failed to make HTTP request: %v", err)
464 }
465 // We expect an error since there's no git repository, but the request should be processed
466 if resp.StatusCode != http.StatusInternalServerError {
467 t.Errorf("Expected status 500, got: %d", resp.StatusCode)
468 }
469}
470
471func TestGitShowHandler(t *testing.T) {
472 // Create a mock agent
473 mockAgent := &mockAgent{
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000474 workingDir: t.TempDir(), // Use a temp directory
475 branchPrefix: "sketch/",
Philip Zeyligerd3ac1122025-05-14 02:54:18 +0000476 }
477
478 // Create the server with the mock agent
479 server, err := server.New(mockAgent, nil)
480 if err != nil {
481 t.Fatalf("Failed to create server: %v", err)
482 }
483
484 // Create a test HTTP server
485 testServer := httptest.NewServer(server)
486 defer testServer.Close()
487
488 // Test missing parameter
489 resp, err := http.Get(testServer.URL + "/git/show")
490 if err != nil {
491 t.Fatalf("Failed to make HTTP request: %v", err)
492 }
493 if resp.StatusCode != http.StatusBadRequest {
494 t.Errorf("Expected status bad request, got: %d", resp.StatusCode)
495 }
496
497 // Test with hash parameter (this will fail due to no git repo, but we're testing the API, not git)
498 resp, err = http.Get(testServer.URL + "/git/show?hash=HEAD")
499 if err != nil {
500 t.Fatalf("Failed to make HTTP request: %v", err)
501 }
502 // We expect an error since there's no git repository, but the request should be processed
503 if resp.StatusCode != http.StatusInternalServerError {
504 t.Errorf("Expected status 500, got: %d", resp.StatusCode)
505 }
506}
507
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700508func TestCompactHandler(t *testing.T) {
509 // Test that mock CompactConversation works
510 mockAgent := &mockAgent{
511 messages: []loop.AgentMessage{},
512 messageCount: 0,
513 sessionID: "test-session",
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000514 branchPrefix: "sketch/",
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700515 }
516
517 ctx := context.Background()
518 err := mockAgent.CompactConversation(ctx)
519 if err != nil {
520 t.Errorf("Mock CompactConversation failed: %v", err)
521 }
522
523 // No HTTP endpoint to test anymore - compaction is done via /compact message
524 t.Log("Mock CompactConversation works correctly")
525}
Sean McCullough138ec242025-06-02 22:42:06 +0000526
Philip Zeyligera9710d72025-07-02 02:50:14 +0000527func TestParsePortProxyHost(t *testing.T) {
528 tests := []struct {
529 name string
530 host string
531 wantPort string
532 }{
533 {
534 name: "valid port proxy host",
535 host: "p8000.localhost",
536 wantPort: "8000",
537 },
538 {
539 name: "valid port proxy host with port suffix",
540 host: "p8000.localhost:8080",
541 wantPort: "8000",
542 },
543 {
544 name: "different port",
545 host: "p3000.localhost",
546 wantPort: "3000",
547 },
548 {
549 name: "regular localhost",
550 host: "localhost",
551 wantPort: "",
552 },
553 {
554 name: "different domain",
555 host: "p8000.example.com",
556 wantPort: "",
557 },
558 {
559 name: "missing p prefix",
560 host: "8000.localhost",
561 wantPort: "",
562 },
563 {
564 name: "invalid port",
565 host: "pabc.localhost",
566 wantPort: "",
567 },
568 {
569 name: "just p prefix",
570 host: "p.localhost",
571 wantPort: "",
572 },
573 {
574 name: "port too high",
575 host: "p99999.localhost",
576 wantPort: "",
577 },
578 {
579 name: "port zero",
580 host: "p0.localhost",
581 wantPort: "",
582 },
583 {
584 name: "negative port",
585 host: "p-1.localhost",
586 wantPort: "",
587 },
588 }
589
590 // Create a test server to access the method
591 s, err := server.New(nil, nil)
592 if err != nil {
593 t.Fatalf("Failed to create server: %v", err)
594 }
595
596 for _, tt := range tests {
597 t.Run(tt.name, func(t *testing.T) {
598 gotPort := s.ParsePortProxyHost(tt.host)
599 if gotPort != tt.wantPort {
600 t.Errorf("parsePortProxyHost(%q) = %q, want %q", tt.host, gotPort, tt.wantPort)
601 }
602 })
603 }
604}
Philip Zeyliger5f26a342025-07-04 01:30:29 +0000605
606// TestStateEndpointIncludesPorts tests that the /state endpoint includes port information
607func TestStateEndpointIncludesPorts(t *testing.T) {
608 mockAgent := &mockAgent{
609 messages: []loop.AgentMessage{},
610 messageCount: 0,
611 currentState: "initial",
612 subscribers: []chan *loop.AgentMessage{},
613 gitUsername: "test-user",
614 initialCommit: "abc123",
615 branchName: "test-branch",
616 branchPrefix: "test-",
617 workingDir: "/tmp/test",
618 sessionID: "test-session",
619 slug: "test-slug",
620 skabandAddr: "http://localhost:8080",
621 }
622
623 // Create a test server
624 server, err := server.New(mockAgent, nil)
625 if err != nil {
626 t.Fatal(err)
627 }
628
629 // Create a test request to the /state endpoint
630 req, err := http.NewRequest("GET", "/state", nil)
631 if err != nil {
632 t.Fatal(err)
633 }
634
635 // Create a response recorder
636 rr := httptest.NewRecorder()
637
638 // Execute the request
639 server.ServeHTTP(rr, req)
640
641 // Check the response
642 if status := rr.Code; status != http.StatusOK {
643 t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
644 }
645
646 // Check that the response contains port information
647 responseBody := rr.Body.String()
648 t.Logf("Response body: %s", responseBody)
649
650 // Verify the response contains the expected ports
651 if !strings.Contains(responseBody, `"open_ports"`) {
652 t.Error("Response should contain 'open_ports' field")
653 }
654
655 if !strings.Contains(responseBody, `"port": 22`) {
656 t.Error("Response should contain port 22 from mock")
657 }
658
659 if !strings.Contains(responseBody, `"port": 80`) {
660 t.Error("Response should contain port 80 from mock")
661 }
662
663 if !strings.Contains(responseBody, `"port": 8080`) {
664 t.Error("Response should contain port 8080 from mock")
665 }
666
667 if !strings.Contains(responseBody, `"process": "sshd"`) {
668 t.Error("Response should contain process name 'sshd'")
669 }
670
671 if !strings.Contains(responseBody, `"process": "nginx"`) {
672 t.Error("Response should contain process name 'nginx'")
673 }
674
675 if !strings.Contains(responseBody, `"proto": "tcp"`) {
676 t.Error("Response should contain protocol 'tcp'")
677 }
678
679 t.Log("State endpoint includes port information correctly")
680}