blob: 04a18bad7b8540b66d04b6bbc90ecea85e936553 [file] [log] [blame]
Philip Zeyliger25f6ff12025-05-02 04:24:10 +00001package server_test
2
3import (
4 "bufio"
5 "context"
Philip Zeyliger254c49f2025-07-17 17:26:24 -07006 "io"
Philip Zeyliger25f6ff12025-05-02 04:24:10 +00007 "net/http"
8 "net/http/httptest"
9 "slices"
10 "strings"
11 "sync"
12 "testing"
13 "time"
14
15 "sketch.dev/llm/conversation"
16 "sketch.dev/loop"
17 "sketch.dev/loop/server"
Philip Zeyliger5f26a342025-07-04 01:30:29 +000018 "tailscale.com/portlist"
Philip Zeyliger25f6ff12025-05-02 04:24:10 +000019)
20
21// mockAgent is a mock implementation of loop.CodingAgent for testing
22type mockAgent struct {
Philip Zeyligereab12de2025-05-14 02:35:53 +000023 mu sync.RWMutex
24 messages []loop.AgentMessage
25 messageCount int
26 currentState string
27 subscribers []chan *loop.AgentMessage
28 stateTransitionListeners []chan loop.StateTransition
bankseancad67b02025-06-27 21:57:05 +000029 gitUsername string
Philip Zeyligereab12de2025-05-14 02:35:53 +000030 initialCommit string
Philip Zeyligereab12de2025-05-14 02:35:53 +000031 branchName string
Philip Zeyligerbe7802a2025-06-04 20:15:25 +000032 branchPrefix string
Philip Zeyligerd3ac1122025-05-14 02:54:18 +000033 workingDir string
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070034 sessionID string
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -070035 slug string
36 retryNumber int
Philip Zeyliger0113be52025-06-07 23:53:41 +000037 skabandAddr string
Philip Zeyliger25f6ff12025-05-02 04:24:10 +000038}
39
banksean5ab8fb82025-07-09 12:34:55 -070040// TokenContextWindow implements loop.CodingAgent.
41func (m *mockAgent) TokenContextWindow() int {
42 return 200000
43}
44
Philip Zeyliger25f6ff12025-05-02 04:24:10 +000045func (m *mockAgent) NewIterator(ctx context.Context, nextMessageIdx int) loop.MessageIterator {
46 m.mu.RLock()
47 // Send existing messages that should be available immediately
48 ch := make(chan *loop.AgentMessage, 100)
49 iter := &mockIterator{
50 agent: m,
51 ctx: ctx,
52 nextMessageIdx: nextMessageIdx,
53 ch: ch,
54 }
55 m.mu.RUnlock()
56 return iter
57}
58
59type mockIterator struct {
60 agent *mockAgent
61 ctx context.Context
62 nextMessageIdx int
63 ch chan *loop.AgentMessage
64 subscribed bool
65}
66
67func (m *mockIterator) Next() *loop.AgentMessage {
68 if !m.subscribed {
69 m.agent.mu.Lock()
70 m.agent.subscribers = append(m.agent.subscribers, m.ch)
71 m.agent.mu.Unlock()
72 m.subscribed = true
73 }
74
75 for {
76 select {
77 case <-m.ctx.Done():
78 return nil
79 case msg := <-m.ch:
80 return msg
81 }
82 }
83}
84
85func (m *mockIterator) Close() {
86 // Remove from subscribers using slices.Delete
87 m.agent.mu.Lock()
88 for i, ch := range m.agent.subscribers {
89 if ch == m.ch {
90 m.agent.subscribers = slices.Delete(m.agent.subscribers, i, i+1)
91 break
92 }
93 }
94 m.agent.mu.Unlock()
95 close(m.ch)
96}
97
98func (m *mockAgent) Messages(start int, end int) []loop.AgentMessage {
99 m.mu.RLock()
100 defer m.mu.RUnlock()
101
102 if start >= len(m.messages) || end > len(m.messages) || start < 0 || end < 0 {
103 return []loop.AgentMessage{}
104 }
105 return slices.Clone(m.messages[start:end])
106}
107
108func (m *mockAgent) MessageCount() int {
109 m.mu.RLock()
110 defer m.mu.RUnlock()
111 return m.messageCount
112}
113
114func (m *mockAgent) AddMessage(msg loop.AgentMessage) {
115 m.mu.Lock()
116 msg.Idx = m.messageCount
117 m.messages = append(m.messages, msg)
118 m.messageCount++
119
120 // Create a copy of subscribers to avoid holding the lock while sending
121 subscribers := make([]chan *loop.AgentMessage, len(m.subscribers))
122 copy(subscribers, m.subscribers)
123 m.mu.Unlock()
124
125 // Notify subscribers
126 msgCopy := msg // Create a copy to avoid race conditions
127 for _, ch := range subscribers {
128 ch <- &msgCopy
129 }
130}
131
Philip Zeyligereab12de2025-05-14 02:35:53 +0000132func (m *mockAgent) NewStateTransitionIterator(ctx context.Context) loop.StateTransitionIterator {
133 m.mu.Lock()
134 ch := make(chan loop.StateTransition, 10)
135 m.stateTransitionListeners = append(m.stateTransitionListeners, ch)
136 m.mu.Unlock()
137
138 return &mockStateTransitionIterator{
139 agent: m,
140 ctx: ctx,
141 ch: ch,
142 }
143}
144
145type mockStateTransitionIterator struct {
146 agent *mockAgent
147 ctx context.Context
148 ch chan loop.StateTransition
149}
150
151func (m *mockStateTransitionIterator) Next() *loop.StateTransition {
152 select {
153 case <-m.ctx.Done():
154 return nil
155 case transition, ok := <-m.ch:
156 if !ok {
157 return nil
158 }
159 transitionCopy := transition
160 return &transitionCopy
161 }
162}
163
164func (m *mockStateTransitionIterator) Close() {
165 m.agent.mu.Lock()
166 for i, ch := range m.agent.stateTransitionListeners {
167 if ch == m.ch {
168 m.agent.stateTransitionListeners = slices.Delete(m.agent.stateTransitionListeners, i, i+1)
169 break
170 }
171 }
172 m.agent.mu.Unlock()
173 close(m.ch)
174}
175
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000176func (m *mockAgent) CurrentStateName() string {
177 m.mu.RLock()
178 defer m.mu.RUnlock()
179 return m.currentState
180}
181
Philip Zeyligereab12de2025-05-14 02:35:53 +0000182func (m *mockAgent) TriggerStateTransition(from, to loop.State, event loop.TransitionEvent) {
183 m.mu.Lock()
184 m.currentState = to.String()
185 transition := loop.StateTransition{
186 From: from,
187 To: to,
188 Event: event,
189 }
190
191 // Create a copy of listeners to avoid holding the lock while sending
192 listeners := make([]chan loop.StateTransition, len(m.stateTransitionListeners))
193 copy(listeners, m.stateTransitionListeners)
194 m.mu.Unlock()
195
196 // Notify listeners
197 for _, ch := range listeners {
198 ch <- transition
199 }
200}
201
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000202func (m *mockAgent) InitialCommit() string {
203 m.mu.RLock()
204 defer m.mu.RUnlock()
205 return m.initialCommit
206}
207
Philip Zeyliger49edc922025-05-14 09:45:45 -0700208func (m *mockAgent) SketchGitBase() string {
209 m.mu.RLock()
210 defer m.mu.RUnlock()
211 return m.initialCommit
212}
213
Philip Zeyligerd3ac1122025-05-14 02:54:18 +0000214func (m *mockAgent) SketchGitBaseRef() string {
215 return "sketch-base-test-session"
216}
217
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000218func (m *mockAgent) BranchName() string {
219 m.mu.RLock()
220 defer m.mu.RUnlock()
221 return m.branchName
222}
223
224// Other required methods of loop.CodingAgent with minimal implementation
225func (m *mockAgent) Init(loop.AgentInit) error { return nil }
226func (m *mockAgent) Ready() <-chan struct{} { ch := make(chan struct{}); close(ch); return ch }
227func (m *mockAgent) URL() string { return "http://localhost:8080" }
228func (m *mockAgent) UserMessage(ctx context.Context, msg string) {}
229func (m *mockAgent) Loop(ctx context.Context) {}
230func (m *mockAgent) CancelTurn(cause error) {}
231func (m *mockAgent) CancelToolUse(id string, cause error) error { return nil }
232func (m *mockAgent) TotalUsage() conversation.CumulativeUsage { return conversation.CumulativeUsage{} }
233func (m *mockAgent) OriginalBudget() conversation.Budget { return conversation.Budget{} }
Philip Zeyligerd3ac1122025-05-14 02:54:18 +0000234func (m *mockAgent) WorkingDir() string { return m.workingDir }
Josh Bleecher Snyderc5848f32025-05-28 18:50:58 +0000235func (m *mockAgent) RepoRoot() string { return m.workingDir }
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000236func (m *mockAgent) Diff(commit *string) (string, error) { return "", nil }
237func (m *mockAgent) OS() string { return "linux" }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700238func (m *mockAgent) SessionID() string { return m.sessionID }
philip.zeyliger8773e682025-06-11 21:36:21 -0700239func (m *mockAgent) SSHConnectionString() string { return "sketch-" + m.sessionID }
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000240func (m *mockAgent) BranchPrefix() string { return m.branchPrefix }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700241func (m *mockAgent) CurrentTodoContent() string { return "" } // Mock returns empty for simplicity
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000242func (m *mockAgent) OutstandingLLMCallCount() int { return 0 }
243func (m *mockAgent) OutstandingToolCalls() []string { return nil }
244func (m *mockAgent) OutsideOS() string { return "linux" }
245func (m *mockAgent) OutsideHostname() string { return "test-host" }
246func (m *mockAgent) OutsideWorkingDir() string { return "/app" }
247func (m *mockAgent) GitOrigin() string { return "" }
bankseancad67b02025-06-27 21:57:05 +0000248func (m *mockAgent) GitUsername() string { return m.gitUsername }
Philip Zeyliger254c49f2025-07-17 17:26:24 -0700249func (m *mockAgent) PassthroughUpstream() bool { return false }
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000250func (m *mockAgent) OpenBrowser(url string) {}
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700251func (m *mockAgent) CompactConversation(ctx context.Context) error {
252 // Mock implementation - just return nil
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000253 return nil
254}
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700255func (m *mockAgent) IsInContainer() bool { return false }
256func (m *mockAgent) FirstMessageIndex() int { return 0 }
257func (m *mockAgent) DetectGitChanges(ctx context.Context) error { return nil }
Philip Zeyligerb5739402025-06-02 07:04:34 -0700258
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700259func (m *mockAgent) Slug() string {
260 m.mu.RLock()
261 defer m.mu.RUnlock()
262 return m.slug
263}
264
265func (m *mockAgent) IncrementRetryNumber() {
266 m.mu.Lock()
267 defer m.mu.Unlock()
268 m.retryNumber++
269}
270
Philip Zeyligerda623b52025-07-04 01:12:38 +0000271func (m *mockAgent) SkabandAddr() string { return m.skabandAddr }
272func (m *mockAgent) LinkToGitHub() bool { return false }
273func (m *mockAgent) DiffStats() (int, int) { return 0, 0 }
Philip Zeyliger5f26a342025-07-04 01:30:29 +0000274func (m *mockAgent) GetPorts() []portlist.Port {
275 // Mock returns a few test ports
276 return []portlist.Port{
277 {Proto: "tcp", Port: 22, Process: "sshd", Pid: 1234},
278 {Proto: "tcp", Port: 80, Process: "nginx", Pid: 5678},
279 {Proto: "tcp", Port: 8080, Process: "test-server", Pid: 9012},
280 }
281}
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700282
283// TestSSEStream tests the SSE stream endpoint
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000284func TestSSEStream(t *testing.T) {
285 // Create a mock agent with initial messages
286 mockAgent := &mockAgent{
Philip Zeyligereab12de2025-05-14 02:35:53 +0000287 messages: []loop.AgentMessage{},
288 messageCount: 0,
289 currentState: "Ready",
290 subscribers: []chan *loop.AgentMessage{},
291 stateTransitionListeners: []chan loop.StateTransition{},
292 initialCommit: "abcd1234",
Philip Zeyligereab12de2025-05-14 02:35:53 +0000293 branchName: "sketch/test-branch",
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000294 branchPrefix: "sketch/",
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700295 slug: "test-slug",
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000296 }
297
298 // Add the initial messages before creating the server
299 // to ensure they're available in the Messages slice
300 msg1 := loop.AgentMessage{
301 Type: loop.UserMessageType,
302 Content: "Hello, this is a test message",
303 Timestamp: time.Now(),
304 }
305 mockAgent.messages = append(mockAgent.messages, msg1)
306 msg1.Idx = mockAgent.messageCount
307 mockAgent.messageCount++
308
309 msg2 := loop.AgentMessage{
310 Type: loop.AgentMessageType,
311 Content: "This is a response message",
312 Timestamp: time.Now(),
313 EndOfTurn: true,
314 }
315 mockAgent.messages = append(mockAgent.messages, msg2)
316 msg2.Idx = mockAgent.messageCount
317 mockAgent.messageCount++
318
319 // Create a server with the mock agent
320 srv, err := server.New(mockAgent, nil)
321 if err != nil {
322 t.Fatalf("Failed to create server: %v", err)
323 }
324
325 // Create a test server
326 ts := httptest.NewServer(srv)
327 defer ts.Close()
328
329 // Create a context with cancellation for the client request
330 ctx, cancel := context.WithCancel(context.Background())
331
332 // Create a request to the /stream endpoint
333 req, err := http.NewRequestWithContext(ctx, "GET", ts.URL+"/stream?from=0", nil)
334 if err != nil {
335 t.Fatalf("Failed to create request: %v", err)
336 }
337
338 // Execute the request
339 res, err := http.DefaultClient.Do(req)
340 if err != nil {
341 t.Fatalf("Failed to execute request: %v", err)
342 }
343 defer res.Body.Close()
344
345 // Check response status
346 if res.StatusCode != http.StatusOK {
347 t.Fatalf("Expected status OK, got %v", res.Status)
348 }
349
350 // Check content type
351 if contentType := res.Header.Get("Content-Type"); contentType != "text/event-stream" {
352 t.Fatalf("Expected Content-Type text/event-stream, got %s", contentType)
353 }
354
355 // Read response events using a scanner
356 scanner := bufio.NewScanner(res.Body)
357
358 // Track events received
359 eventsReceived := map[string]int{
360 "state": 0,
361 "message": 0,
362 "heartbeat": 0,
363 }
364
365 // Read for a short time to capture initial state and messages
366 dataLines := []string{}
367 eventType := ""
368
369 go func() {
370 // After reading for a while, add a new message to test real-time updates
371 time.Sleep(500 * time.Millisecond)
372
373 mockAgent.AddMessage(loop.AgentMessage{
374 Type: loop.ToolUseMessageType,
375 Content: "This is a new real-time message",
376 Timestamp: time.Now(),
377 ToolName: "test_tool",
378 })
379
Philip Zeyligereab12de2025-05-14 02:35:53 +0000380 // Trigger a state transition to test state updates
381 time.Sleep(200 * time.Millisecond)
382 mockAgent.TriggerStateTransition(loop.StateReady, loop.StateSendingToLLM, loop.TransitionEvent{
383 Description: "Agent started thinking",
384 Data: "start_thinking",
385 })
386
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000387 // Let it process for longer
388 time.Sleep(1000 * time.Millisecond)
389 cancel() // Cancel to end the test
390 }()
391
392 // Read events
393 for scanner.Scan() {
394 line := scanner.Text()
395
396 if strings.HasPrefix(line, "event: ") {
397 eventType = strings.TrimPrefix(line, "event: ")
398 eventsReceived[eventType]++
399 } else if strings.HasPrefix(line, "data: ") {
400 dataLines = append(dataLines, line)
401 } else if line == "" && eventType != "" {
402 // End of event
403 eventType = ""
404 }
405
406 // Break if context is done
407 if ctx.Err() != nil {
408 break
409 }
410 }
411
412 if err := scanner.Err(); err != nil && ctx.Err() == nil {
413 t.Fatalf("Scanner error: %v", err)
414 }
415
416 // Simplified validation - just make sure we received something
417 t.Logf("Events received: %v", eventsReceived)
418 t.Logf("Data lines received: %d", len(dataLines))
419
420 // Basic validation that we received at least some events
421 if eventsReceived["state"] == 0 && eventsReceived["message"] == 0 {
422 t.Errorf("Did not receive any events")
423 }
424}
Philip Zeyligerd3ac1122025-05-14 02:54:18 +0000425
426func TestGitRawDiffHandler(t *testing.T) {
427 // Create a mock agent
428 mockAgent := &mockAgent{
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000429 workingDir: t.TempDir(), // Use a temp directory
430 branchPrefix: "sketch/",
Philip Zeyligerd3ac1122025-05-14 02:54:18 +0000431 }
432
433 // Create the server with the mock agent
434 server, err := server.New(mockAgent, nil)
435 if err != nil {
436 t.Fatalf("Failed to create server: %v", err)
437 }
438
439 // Create a test HTTP server
440 testServer := httptest.NewServer(server)
441 defer testServer.Close()
442
443 // Test missing parameters
444 resp, err := http.Get(testServer.URL + "/git/rawdiff")
445 if err != nil {
446 t.Fatalf("Failed to make HTTP request: %v", err)
447 }
448 if resp.StatusCode != http.StatusBadRequest {
449 t.Errorf("Expected status bad request, got: %d", resp.StatusCode)
450 }
451
452 // Test with commit parameter (this will fail due to no git repo, but we're testing the API, not git)
453 resp, err = http.Get(testServer.URL + "/git/rawdiff?commit=HEAD")
454 if err != nil {
455 t.Fatalf("Failed to make HTTP request: %v", err)
456 }
457 // We expect an error since there's no git repository, but the request should be processed
458 if resp.StatusCode != http.StatusInternalServerError {
459 t.Errorf("Expected status 500, got: %d", resp.StatusCode)
460 }
461
462 // Test with from/to parameters
463 resp, err = http.Get(testServer.URL + "/git/rawdiff?from=HEAD~1&to=HEAD")
464 if err != nil {
465 t.Fatalf("Failed to make HTTP request: %v", err)
466 }
467 // We expect an error since there's no git repository, but the request should be processed
468 if resp.StatusCode != http.StatusInternalServerError {
469 t.Errorf("Expected status 500, got: %d", resp.StatusCode)
470 }
471}
472
473func TestGitShowHandler(t *testing.T) {
474 // Create a mock agent
475 mockAgent := &mockAgent{
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000476 workingDir: t.TempDir(), // Use a temp directory
477 branchPrefix: "sketch/",
Philip Zeyligerd3ac1122025-05-14 02:54:18 +0000478 }
479
480 // Create the server with the mock agent
481 server, err := server.New(mockAgent, nil)
482 if err != nil {
483 t.Fatalf("Failed to create server: %v", err)
484 }
485
486 // Create a test HTTP server
487 testServer := httptest.NewServer(server)
488 defer testServer.Close()
489
490 // Test missing parameter
491 resp, err := http.Get(testServer.URL + "/git/show")
492 if err != nil {
493 t.Fatalf("Failed to make HTTP request: %v", err)
494 }
495 if resp.StatusCode != http.StatusBadRequest {
496 t.Errorf("Expected status bad request, got: %d", resp.StatusCode)
497 }
498
499 // Test with hash parameter (this will fail due to no git repo, but we're testing the API, not git)
500 resp, err = http.Get(testServer.URL + "/git/show?hash=HEAD")
501 if err != nil {
502 t.Fatalf("Failed to make HTTP request: %v", err)
503 }
504 // We expect an error since there's no git repository, but the request should be processed
505 if resp.StatusCode != http.StatusInternalServerError {
506 t.Errorf("Expected status 500, got: %d", resp.StatusCode)
507 }
508}
509
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700510func TestCompactHandler(t *testing.T) {
511 // Test that mock CompactConversation works
512 mockAgent := &mockAgent{
513 messages: []loop.AgentMessage{},
514 messageCount: 0,
515 sessionID: "test-session",
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000516 branchPrefix: "sketch/",
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700517 }
518
519 ctx := context.Background()
520 err := mockAgent.CompactConversation(ctx)
521 if err != nil {
522 t.Errorf("Mock CompactConversation failed: %v", err)
523 }
524
525 // No HTTP endpoint to test anymore - compaction is done via /compact message
526 t.Log("Mock CompactConversation works correctly")
527}
Sean McCullough138ec242025-06-02 22:42:06 +0000528
Philip Zeyligera9710d72025-07-02 02:50:14 +0000529func TestParsePortProxyHost(t *testing.T) {
530 tests := []struct {
531 name string
532 host string
533 wantPort string
534 }{
535 {
536 name: "valid port proxy host",
537 host: "p8000.localhost",
538 wantPort: "8000",
539 },
540 {
541 name: "valid port proxy host with port suffix",
542 host: "p8000.localhost:8080",
543 wantPort: "8000",
544 },
545 {
546 name: "different port",
547 host: "p3000.localhost",
548 wantPort: "3000",
549 },
550 {
551 name: "regular localhost",
552 host: "localhost",
553 wantPort: "",
554 },
555 {
556 name: "different domain",
557 host: "p8000.example.com",
558 wantPort: "",
559 },
560 {
561 name: "missing p prefix",
562 host: "8000.localhost",
563 wantPort: "",
564 },
565 {
566 name: "invalid port",
567 host: "pabc.localhost",
568 wantPort: "",
569 },
570 {
571 name: "just p prefix",
572 host: "p.localhost",
573 wantPort: "",
574 },
575 {
576 name: "port too high",
577 host: "p99999.localhost",
578 wantPort: "",
579 },
580 {
581 name: "port zero",
582 host: "p0.localhost",
583 wantPort: "",
584 },
585 {
586 name: "negative port",
587 host: "p-1.localhost",
588 wantPort: "",
589 },
590 }
591
592 // Create a test server to access the method
593 s, err := server.New(nil, nil)
594 if err != nil {
595 t.Fatalf("Failed to create server: %v", err)
596 }
597
598 for _, tt := range tests {
599 t.Run(tt.name, func(t *testing.T) {
600 gotPort := s.ParsePortProxyHost(tt.host)
601 if gotPort != tt.wantPort {
602 t.Errorf("parsePortProxyHost(%q) = %q, want %q", tt.host, gotPort, tt.wantPort)
603 }
604 })
605 }
606}
Philip Zeyliger5f26a342025-07-04 01:30:29 +0000607
608// TestStateEndpointIncludesPorts tests that the /state endpoint includes port information
609func TestStateEndpointIncludesPorts(t *testing.T) {
610 mockAgent := &mockAgent{
611 messages: []loop.AgentMessage{},
612 messageCount: 0,
613 currentState: "initial",
614 subscribers: []chan *loop.AgentMessage{},
615 gitUsername: "test-user",
616 initialCommit: "abc123",
617 branchName: "test-branch",
618 branchPrefix: "test-",
619 workingDir: "/tmp/test",
620 sessionID: "test-session",
621 slug: "test-slug",
622 skabandAddr: "http://localhost:8080",
623 }
624
625 // Create a test server
626 server, err := server.New(mockAgent, nil)
627 if err != nil {
628 t.Fatal(err)
629 }
630
631 // Create a test request to the /state endpoint
632 req, err := http.NewRequest("GET", "/state", nil)
633 if err != nil {
634 t.Fatal(err)
635 }
636
637 // Create a response recorder
638 rr := httptest.NewRecorder()
639
640 // Execute the request
641 server.ServeHTTP(rr, req)
642
643 // Check the response
644 if status := rr.Code; status != http.StatusOK {
645 t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
646 }
647
648 // Check that the response contains port information
649 responseBody := rr.Body.String()
650 t.Logf("Response body: %s", responseBody)
651
652 // Verify the response contains the expected ports
653 if !strings.Contains(responseBody, `"open_ports"`) {
654 t.Error("Response should contain 'open_ports' field")
655 }
656
657 if !strings.Contains(responseBody, `"port": 22`) {
658 t.Error("Response should contain port 22 from mock")
659 }
660
661 if !strings.Contains(responseBody, `"port": 80`) {
662 t.Error("Response should contain port 80 from mock")
663 }
664
665 if !strings.Contains(responseBody, `"port": 8080`) {
666 t.Error("Response should contain port 8080 from mock")
667 }
668
669 if !strings.Contains(responseBody, `"process": "sshd"`) {
670 t.Error("Response should contain process name 'sshd'")
671 }
672
673 if !strings.Contains(responseBody, `"process": "nginx"`) {
674 t.Error("Response should contain process name 'nginx'")
675 }
676
677 if !strings.Contains(responseBody, `"proto": "tcp"`) {
678 t.Error("Response should contain protocol 'tcp'")
679 }
680
681 t.Log("State endpoint includes port information correctly")
682}
Philip Zeyliger254c49f2025-07-17 17:26:24 -0700683
684// TestGitPushHandler tests the git push endpoint
685func TestGitPushHandler(t *testing.T) {
686 mockAgent := &mockAgent{
687 workingDir: t.TempDir(),
688 branchPrefix: "sketch/",
689 }
690
691 // Create the server with the mock agent
692 server, err := server.New(mockAgent, nil)
693 if err != nil {
694 t.Fatalf("Failed to create server: %v", err)
695 }
696
697 // Create a test HTTP server
698 testServer := httptest.NewServer(server)
699 defer testServer.Close()
700
701 // Test missing required parameters
702 tests := []struct {
703 name string
704 requestBody string
705 expectedStatus int
706 expectedError string
707 }{
708 {
709 name: "missing all parameters",
710 requestBody: `{}`,
711 expectedStatus: http.StatusBadRequest,
712 expectedError: "Missing required parameters: remote, branch, and commit",
713 },
714 {
715 name: "missing commit parameter",
716 requestBody: `{"remote": "origin", "branch": "main"}`,
717 expectedStatus: http.StatusBadRequest,
718 expectedError: "Missing required parameters: remote, branch, and commit",
719 },
720 {
721 name: "missing remote parameter",
722 requestBody: `{"branch": "main", "commit": "abc123"}`,
723 expectedStatus: http.StatusBadRequest,
724 expectedError: "Missing required parameters: remote, branch, and commit",
725 },
726 {
727 name: "missing branch parameter",
728 requestBody: `{"remote": "origin", "commit": "abc123"}`,
729 expectedStatus: http.StatusBadRequest,
730 expectedError: "Missing required parameters: remote, branch, and commit",
731 },
732 {
733 name: "all parameters present",
734 requestBody: `{"remote": "origin", "branch": "main", "commit": "abc123", "dry_run": true}`,
735 expectedStatus: http.StatusOK, // Parameters are valid, response will be JSON
736 expectedError: "", // No parameter validation error
737 },
738 }
739
740 for _, tt := range tests {
741 t.Run(tt.name, func(t *testing.T) {
742 resp, err := http.Post(
743 testServer.URL+"/git/push",
744 "application/json",
745 strings.NewReader(tt.requestBody),
746 )
747 if err != nil {
748 t.Fatalf("Failed to make HTTP request: %v", err)
749 }
750 defer resp.Body.Close()
751
752 if resp.StatusCode != tt.expectedStatus {
753 t.Errorf("Expected status %d, got: %d", tt.expectedStatus, resp.StatusCode)
754 }
755
756 if tt.expectedError != "" {
757 body, err := io.ReadAll(resp.Body)
758 if err != nil {
759 t.Fatalf("Failed to read response body: %v", err)
760 }
761 if !strings.Contains(string(body), tt.expectedError) {
762 t.Errorf("Expected error message '%s', got: %s", tt.expectedError, string(body))
763 }
764 }
765 })
766 }
767}
768
769// TestGitPushInfoHandler tests the git push info endpoint
770func TestGitPushInfoHandler(t *testing.T) {
771 mockAgent := &mockAgent{
772 workingDir: t.TempDir(),
773 branchPrefix: "sketch/",
774 }
775
776 // Create the server with the mock agent
777 server, err := server.New(mockAgent, nil)
778 if err != nil {
779 t.Fatalf("Failed to create server: %v", err)
780 }
781
782 // Create a test HTTP server
783 testServer := httptest.NewServer(server)
784 defer testServer.Close()
785
786 // Test GET request
787 resp, err := http.Get(testServer.URL + "/git/pushinfo")
788 if err != nil {
789 t.Fatalf("Failed to make HTTP request: %v", err)
790 }
791 defer resp.Body.Close()
792
793 // We expect this to fail with 500 since there's no git repository
794 // but the endpoint should be accessible
795 if resp.StatusCode != http.StatusInternalServerError {
796 t.Errorf("Expected status 500, got: %d", resp.StatusCode)
797 }
798
799 // Test that POST is not allowed
800 resp, err = http.Post(testServer.URL+"/git/pushinfo", "application/json", strings.NewReader(`{}`))
801 if err != nil {
802 t.Fatalf("Failed to make HTTP request: %v", err)
803 }
804 defer resp.Body.Close()
805
806 if resp.StatusCode != http.StatusMethodNotAllowed {
807 t.Errorf("Expected status 405, got: %d", resp.StatusCode)
808 }
809}