199 lines
4.6 KiB
Go
199 lines
4.6 KiB
Go
// Package integration provides end-to-end integration tests for the Kandev backend.
|
|
package integration
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
ws "github.com/kandev/kandev/pkg/websocket"
|
|
)
|
|
|
|
// OrchestratorWSClient is a WebSocket client for orchestrator tests
|
|
type OrchestratorWSClient struct {
|
|
conn *websocket.Conn
|
|
t *testing.T
|
|
notifications chan *ws.Message
|
|
done chan struct{}
|
|
// pending tracks in-flight requests: request ID -> response channel
|
|
pending map[string]chan *ws.Message
|
|
// send is the channel for outgoing messages (serialized through writePump)
|
|
send chan []byte
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewOrchestratorWSClient creates a WebSocket connection to the test server
|
|
func NewOrchestratorWSClient(t *testing.T, serverURL string) *OrchestratorWSClient {
|
|
t.Helper()
|
|
|
|
wsURL := "ws" + strings.TrimPrefix(serverURL, "http") + "/ws"
|
|
|
|
dialer := websocket.Dialer{
|
|
HandshakeTimeout: 5 * time.Second,
|
|
}
|
|
|
|
conn, resp, err := dialer.Dial(wsURL, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode)
|
|
|
|
client := &OrchestratorWSClient{
|
|
conn: conn,
|
|
t: t,
|
|
notifications: make(chan *ws.Message, 100),
|
|
done: make(chan struct{}),
|
|
pending: make(map[string]chan *ws.Message),
|
|
send: make(chan []byte, 256),
|
|
}
|
|
|
|
go client.readPump()
|
|
go client.writePump()
|
|
|
|
return client
|
|
}
|
|
|
|
func createOrchestratorWorkspace(t *testing.T, client *OrchestratorWSClient) string {
|
|
t.Helper()
|
|
|
|
resp, err := client.SendRequest("workspace-1", ws.ActionWorkspaceCreate, map[string]interface{}{
|
|
"name": "Test Workspace",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
var payload map[string]interface{}
|
|
require.NoError(t, resp.ParsePayload(&payload))
|
|
|
|
return payload["id"].(string)
|
|
}
|
|
|
|
// readPump reads messages from the WebSocket connection
|
|
func (c *OrchestratorWSClient) readPump() {
|
|
defer close(c.done)
|
|
for {
|
|
_, data, err := c.conn.ReadMessage()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var msg ws.Message
|
|
if err := json.Unmarshal(data, &msg); err != nil {
|
|
continue
|
|
}
|
|
|
|
if msg.Type == ws.MessageTypeNotification {
|
|
select {
|
|
case c.notifications <- &msg:
|
|
default:
|
|
}
|
|
} else {
|
|
// Route response to the pending request by ID
|
|
c.mu.Lock()
|
|
ch, ok := c.pending[msg.ID]
|
|
c.mu.Unlock()
|
|
if ok {
|
|
select {
|
|
case ch <- &msg:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// writePump serializes all writes to the WebSocket connection
|
|
func (c *OrchestratorWSClient) writePump() {
|
|
for data := range c.send {
|
|
if err := c.conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Close closes the WebSocket connection
|
|
func (c *OrchestratorWSClient) Close() {
|
|
close(c.send)
|
|
if err := c.conn.Close(); err != nil {
|
|
c.t.Logf("failed to close websocket: %v", err)
|
|
}
|
|
<-c.done
|
|
}
|
|
|
|
// SendRequest sends a request and waits for a response
|
|
func (c *OrchestratorWSClient) SendRequest(id, action string, payload interface{}) (*ws.Message, error) {
|
|
msg, err := ws.NewRequest(id, action, payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data, err := json.Marshal(msg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create a response channel for this request
|
|
respCh := make(chan *ws.Message, 1)
|
|
|
|
// Register the pending request BEFORE sending (so we don't miss the response)
|
|
c.mu.Lock()
|
|
c.pending[id] = respCh
|
|
c.mu.Unlock()
|
|
|
|
// Ensure we clean up when done
|
|
defer func() {
|
|
c.mu.Lock()
|
|
delete(c.pending, id)
|
|
c.mu.Unlock()
|
|
}()
|
|
|
|
// Send the request through the write pump (serialized)
|
|
select {
|
|
case c.send <- data:
|
|
case <-time.After(5 * time.Second):
|
|
return nil, fmt.Errorf("send buffer full")
|
|
}
|
|
|
|
// Wait for response on our dedicated channel
|
|
select {
|
|
case resp := <-respCh:
|
|
return resp, nil
|
|
case <-time.After(30 * time.Second):
|
|
return nil, context.DeadlineExceeded
|
|
}
|
|
}
|
|
|
|
// WaitForNotification waits for a notification with the given action prefix
|
|
func (c *OrchestratorWSClient) WaitForNotification(actionPrefix string, timeout time.Duration) (*ws.Message, error) {
|
|
deadline := time.After(timeout)
|
|
for {
|
|
select {
|
|
case msg := <-c.notifications:
|
|
if strings.HasPrefix(msg.Action, actionPrefix) {
|
|
return msg, nil
|
|
}
|
|
case <-deadline:
|
|
return nil, context.DeadlineExceeded
|
|
}
|
|
}
|
|
}
|
|
|
|
// CollectNotifications collects all notifications for a duration
|
|
func (c *OrchestratorWSClient) CollectNotifications(duration time.Duration) []*ws.Message {
|
|
var msgs []*ws.Message
|
|
deadline := time.After(duration)
|
|
for {
|
|
select {
|
|
case msg := <-c.notifications:
|
|
msgs = append(msgs, msg)
|
|
case <-deadline:
|
|
return msgs
|
|
}
|
|
}
|
|
}
|