Files
kandev/apps/backend/internal/integration/multi_client_integration_test.go
T
2026-07-24 09:55:18 +01:00

278 lines
8.3 KiB
Go

// Package integration provides end-to-end integration tests for the Kandev backend.
package integration
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
ws "github.com/kandev/kandev/pkg/websocket"
)
func TestMultipleClients(t *testing.T) {
ts := NewTestServer(t)
defer ts.Close()
client1 := NewWSClient(t, ts.Server.URL)
defer client1.Close()
client2 := NewWSClient(t, ts.Server.URL)
defer client2.Close()
workspaceID := createWorkspace(t, client1)
repositoryID := createRepository(t, client1, workspaceID)
// Client 1 creates a workflow
workflowResp, err := client1.SendRequest("c1-workflow-1", ws.ActionWorkflowCreate, map[string]interface{}{
"workspace_id": workspaceID,
"name": "Shared Workflow",
"workflow_template_id": "simple",
})
require.NoError(t, err)
var workflowPayload map[string]interface{}
err = workflowResp.ParsePayload(&workflowPayload)
require.NoError(t, err)
workflowID := workflowPayload["id"].(string)
// Client 2 can see both the workspace's default Kanban workflow and the
// additional workflow created by client 1.
listResp, err := client2.SendRequest("c2-list-1", ws.ActionWorkflowList, map[string]interface{}{
"workspace_id": workspaceID,
})
require.NoError(t, err)
var listPayload map[string]interface{}
err = listResp.ParsePayload(&listPayload)
require.NoError(t, err)
workflows, ok := listPayload["workflows"].([]interface{})
require.True(t, ok)
assert.Len(t, workflows, 2)
// Client 2 lists workflow steps (created automatically with workflow)
stepResp, err := client2.SendRequest("c2-step-list-1", ws.ActionWorkflowStepList, map[string]interface{}{
"workflow_id": workflowID,
})
require.NoError(t, err)
var stepPayload map[string]interface{}
err = stepResp.ParsePayload(&stepPayload)
require.NoError(t, err)
steps := stepPayload["steps"].([]interface{})
require.NotEmpty(t, steps)
workflowStepID := steps[0].(map[string]interface{})["id"].(string)
// Client 1 can also see the workflow steps
stepListResp, err := client1.SendRequest("c1-step-list-1", ws.ActionWorkflowStepList, map[string]interface{}{
"workflow_id": workflowID,
})
require.NoError(t, err)
var stepListPayload map[string]interface{}
err = stepListResp.ParsePayload(&stepListPayload)
require.NoError(t, err)
stepsList, ok := stepListPayload["steps"].([]interface{})
require.True(t, ok)
assert.GreaterOrEqual(t, len(stepsList), 1)
// Client 1 creates a task in the workflow step
taskResp, err := client1.SendRequest("c1-task-1", ws.ActionTaskCreate, map[string]interface{}{
"workspace_id": workspaceID,
"workflow_id": workflowID,
"workflow_step_id": workflowStepID,
"title": "Task by Client 1",
"repository_id": repositoryID,
"base_branch": "main",
})
require.NoError(t, err)
var taskPayload map[string]interface{}
err = taskResp.ParsePayload(&taskPayload)
require.NoError(t, err)
// Client 2 can see the task
taskListResp, err := client2.SendRequest("c2-task-list-1", ws.ActionTaskList, map[string]interface{}{
"workflow_id": workflowID,
})
require.NoError(t, err)
var taskListPayload map[string]interface{}
err = taskListResp.ParsePayload(&taskListPayload)
require.NoError(t, err)
tasks, ok := taskListPayload["tasks"].([]interface{})
require.True(t, ok)
assert.Len(t, tasks, 1)
_ = taskPayload // suppress unused variable warning
}
func TestConcurrentRequests(t *testing.T) {
ts := NewTestServer(t)
defer ts.Close()
client := NewWSClient(t, ts.Server.URL)
defer client.Close()
workspaceID := createWorkspace(t, client)
repositoryID := createRepository(t, client, workspaceID)
// Create workflow (workflow steps are created automatically)
workflowResp, _ := client.SendRequest("workflow-1", ws.ActionWorkflowCreate, map[string]interface{}{
"workspace_id": workspaceID,
"name": "Concurrent Test Workflow",
"workflow_template_id": "simple",
})
var workflowPayload map[string]interface{}
if err := workflowResp.ParsePayload(&workflowPayload); err != nil {
t.Fatalf("failed to parse workflow payload: %v", err)
}
workflowID := workflowPayload["id"].(string)
// Get first workflow step
stepResp, _ := client.SendRequest("step-list-1", ws.ActionWorkflowStepList, map[string]interface{}{
"workflow_id": workflowID,
})
var stepListPayload map[string]interface{}
if err := stepResp.ParsePayload(&stepListPayload); err != nil {
t.Fatalf("failed to parse step list payload: %v", err)
}
steps := stepListPayload["steps"].([]interface{})
workflowStepID := steps[0].(map[string]interface{})["id"].(string)
// Create multiple tasks concurrently
numTasks := 10
var wg sync.WaitGroup
results := make(chan error, numTasks)
for i := 0; i < numTasks; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
// Create separate client for concurrency
c := NewWSClient(t, ts.Server.URL)
defer c.Close()
_, err := c.SendRequest(
"task-"+string(rune('0'+idx)),
ws.ActionTaskCreate,
map[string]interface{}{
"workspace_id": workspaceID,
"workflow_id": workflowID,
"workflow_step_id": workflowStepID,
"title": "Concurrent Task " + string(rune('0'+idx)),
"repository_id": repositoryID,
"base_branch": "main",
},
)
results <- err
}(i)
}
wg.Wait()
close(results)
// Verify all tasks were created
for err := range results {
assert.NoError(t, err)
}
// Verify task count
listResp, err := client.SendRequest("list-1", ws.ActionTaskList, map[string]interface{}{
"workflow_id": workflowID,
})
require.NoError(t, err)
var listPayload map[string]interface{}
err = listResp.ParsePayload(&listPayload)
require.NoError(t, err)
tasks, ok := listPayload["tasks"].([]interface{})
require.True(t, ok)
assert.Len(t, tasks, numTasks)
}
func TestTaskSubscription(t *testing.T) {
ts := NewTestServer(t)
defer ts.Close()
client := NewWSClient(t, ts.Server.URL)
defer client.Close()
workspaceID := createWorkspace(t, client)
repositoryID := createRepository(t, client, workspaceID)
// Create a task first (workflow steps are created automatically with workflow)
workflowResp, _ := client.SendRequest("workflow-1", ws.ActionWorkflowCreate, map[string]interface{}{
"workspace_id": workspaceID,
"name": "Sub Test Workflow",
"workflow_template_id": "simple",
})
var workflowPayload map[string]interface{}
if err := workflowResp.ParsePayload(&workflowPayload); err != nil {
t.Fatalf("failed to parse workflow payload: %v", err)
}
workflowID := workflowPayload["id"].(string)
// Get first workflow step
stepResp, _ := client.SendRequest("step-list-1", ws.ActionWorkflowStepList, map[string]interface{}{
"workflow_id": workflowID,
})
var stepListPayload map[string]interface{}
if err := stepResp.ParsePayload(&stepListPayload); err != nil {
t.Fatalf("failed to parse step list payload: %v", err)
}
steps := stepListPayload["steps"].([]interface{})
workflowStepID := steps[0].(map[string]interface{})["id"].(string)
taskResp, _ := client.SendRequest("task-1", ws.ActionTaskCreate, map[string]interface{}{
"workspace_id": workspaceID,
"workflow_id": workflowID,
"workflow_step_id": workflowStepID,
"title": "Subscribable Task",
"repository_id": repositoryID,
"base_branch": "main",
})
var taskPayload map[string]interface{}
if err := taskResp.ParsePayload(&taskPayload); err != nil {
t.Fatalf("failed to parse task payload: %v", err)
}
taskID := taskPayload["id"].(string)
// Subscribe to the task
t.Run("Subscribe", func(t *testing.T) {
resp, err := client.SendRequest("sub-1", ws.ActionTaskSubscribe, map[string]interface{}{
"task_id": taskID,
})
require.NoError(t, err)
assert.Equal(t, ws.MessageTypeResponse, resp.Type)
var payload map[string]interface{}
err = resp.ParsePayload(&payload)
require.NoError(t, err)
assert.Equal(t, true, payload["success"])
assert.Equal(t, taskID, payload["task_id"])
})
// Unsubscribe from the task
t.Run("Unsubscribe", func(t *testing.T) {
resp, err := client.SendRequest("unsub-1", ws.ActionTaskUnsubscribe, map[string]interface{}{
"task_id": taskID,
})
require.NoError(t, err)
assert.Equal(t, ws.MessageTypeResponse, resp.Type)
var payload map[string]interface{}
err = resp.ParsePayload(&payload)
require.NoError(t, err)
assert.Equal(t, true, payload["success"])
})
}