package temporal import ( "bytes" "encoding/json" "fmt" "net" "net/http" "net/http/httptest" "testing" "time" ) // isTemporalAvailable checks if Temporal gRPC server is reachable func isTemporalAvailable() bool { conn, err := net.DialTimeout("tcp", "localhost:7233", 1*time.Second) if err != nil { return false } conn.Close() return true } // TestIntegration_CompleteWorkflowLifecycle simulates a complete workflow lifecycle func TestIntegration_CompleteWorkflowLifecycle(t *testing.T) { if !isTemporalAvailable() { t.Skip("Temporal server not available"); return } handler := NewHandler("localhost:7233") // Step 1: Start workflow startReq := RequestPayload{ Action: "START_WORKFLOW", Namespace: "default", Payload: map[string]interface{}{ "workflow_id": "lifecycle_test_1", "workflow_type": "OrderProcessing", "task_queue": "orders", "input": map[string]interface{}{ "order_id": "12345", "amount": 99.99, }, }, } body, _ := json.Marshal(startReq) req := httptest.NewRequest("POST", "/workflow", bytes.NewReader(body)) w := httptest.NewRecorder() handler.handleWorkflow(w, req) if w.Code != http.StatusOK { t.Fatalf("START_WORKFLOW failed with status %d", w.Code) } var startResp ResponsePayload json.NewDecoder(w.Body).Decode(&startResp) if !startResp.Success || startResp.Data == nil { t.Fatal("START_WORKFLOW response invalid") } startData := startResp.Data.(map[string]interface{}) workflowID := startData["workflow_id"].(string) // Step 2: Describe workflow describeReq := RequestPayload{ Action: "DESCRIBE_WORKFLOW", Namespace: "default", Payload: map[string]interface{}{ "workflow_id": workflowID, }, } body, _ = json.Marshal(describeReq) req = httptest.NewRequest("POST", "/workflow", bytes.NewReader(body)) w = httptest.NewRecorder() handler.handleWorkflow(w, req) if w.Code != http.StatusOK { t.Fatalf("DESCRIBE_WORKFLOW failed with status %d", w.Code) } // Step 3: Signal workflow signalReq := RequestPayload{ Action: "SIGNAL_WORKFLOW", Namespace: "default", Payload: map[string]interface{}{ "workflow_id": workflowID, "signal_name": "payment_received", "input": map[string]interface{}{ "amount": 99.99, }, }, } body, _ = json.Marshal(signalReq) req = httptest.NewRequest("POST", "/workflow", bytes.NewReader(body)) w = httptest.NewRecorder() handler.handleWorkflow(w, req) if w.Code != http.StatusOK { t.Fatalf("SIGNAL_WORKFLOW failed with status %d", w.Code) } // Step 4: Query workflow queryReq := RequestPayload{ Action: "QUERY_WORKFLOW", Namespace: "default", Payload: map[string]interface{}{ "workflow_id": workflowID, "query_type": "get_status", }, } body, _ = json.Marshal(queryReq) req = httptest.NewRequest("POST", "/workflow", bytes.NewReader(body)) w = httptest.NewRecorder() handler.handleWorkflow(w, req) if w.Code != http.StatusOK { t.Fatalf("QUERY_WORKFLOW failed with status %d", w.Code) } // Step 5: Terminate workflow terminateReq := RequestPayload{ Action: "TERMINATE_WORKFLOW", Namespace: "default", Payload: map[string]interface{}{ "workflow_id": workflowID, "reason": "Order completed", }, } body, _ = json.Marshal(terminateReq) req = httptest.NewRequest("POST", "/workflow", bytes.NewReader(body)) w = httptest.NewRecorder() handler.handleWorkflow(w, req) if w.Code != http.StatusOK { t.Fatalf("TERMINATE_WORKFLOW failed with status %d", w.Code) } t.Logf("Complete workflow lifecycle test passed: %s", workflowID) } // TestIntegration_MultipleNamespaces tests operations across different namespaces func TestIntegration_MultipleNamespaces(t *testing.T) { if !isTemporalAvailable() { t.Skip("Temporal server not available"); return } handler := NewHandler("localhost:7233") namespaces := []string{"default", "production", "staging"} for _, ns := range namespaces { t.Run("namespace_"+ns, func(t *testing.T) { req := RequestPayload{ Action: "DESCRIBE_NAMESPACE", Namespace: ns, } body, _ := json.Marshal(req) httpReq := httptest.NewRequest("POST", "/workflow", bytes.NewReader(body)) w := httptest.NewRecorder() handler.handleWorkflow(w, httpReq) if w.Code != http.StatusOK { t.Errorf("DESCRIBE_NAMESPACE failed for %s", ns) } var resp ResponsePayload json.NewDecoder(w.Body).Decode(&resp) if resp.Namespace != ns { t.Errorf("Expected namespace %s, got %s", ns, resp.Namespace) } }) } } // TestIntegration_LargePayload tests handling of large input payloads func TestIntegration_LargePayload(t *testing.T) { if !isTemporalAvailable() { t.Skip("Temporal server not available"); return } handler := NewHandler("localhost:7233") // Create large input payload largeInput := make(map[string]interface{}) for i := 0; i < 100; i++ { largeInput[string(rune('a'+i%26))+string(rune(i))] = "value_" + string(rune(i)) } req := RequestPayload{ Action: "START_WORKFLOW", Namespace: "default", Payload: map[string]interface{}{ "workflow_id": "large_payload_test", "workflow_type": "TestWorkflow", "task_queue": "default", "input": largeInput, }, } body, _ := json.Marshal(req) httpReq := httptest.NewRequest("POST", "/workflow", bytes.NewReader(body)) w := httptest.NewRecorder() handler.handleWorkflow(w, httpReq) if w.Code != http.StatusOK { t.Fatalf("Large payload test failed with status %d", w.Code) } var resp ResponsePayload json.NewDecoder(w.Body).Decode(&resp) if !resp.Success { t.Fatal("Large payload request failed") } } // TestIntegration_ConcurrentRequests tests handling of concurrent requests func TestIntegration_ConcurrentRequests(t *testing.T) { if !isTemporalAvailable() { t.Skip("Temporal server not available"); return } handler := NewHandler("localhost:7233") numRequests := 10 results := make(chan error, numRequests) for i := 0; i < numRequests; i++ { go func(idx int) { req := RequestPayload{ Action: "START_WORKFLOW", Namespace: "default", Payload: map[string]interface{}{ "workflow_id": "concurrent_" + string(rune('a'+idx)), "workflow_type": "ConcurrentTest", "task_queue": "default", }, } body, _ := json.Marshal(req) httpReq := httptest.NewRequest("POST", "/workflow", bytes.NewReader(body)) w := httptest.NewRecorder() handler.handleWorkflow(w, httpReq) if w.Code != http.StatusOK { results <- fmt.Errorf("request %d failed with status %d", idx, w.Code) } else { results <- nil } }(i) } // Wait for all results for i := 0; i < numRequests; i++ { if err := <-results; err != nil { t.Error(err) } } t.Logf("Concurrent requests test passed: %d requests", numRequests) } // TestIntegration_ErrorRecovery tests error recovery mechanisms func TestIntegration_ErrorRecovery(t *testing.T) { if !isTemporalAvailable() { t.Skip("Temporal server not available"); return } handler := NewHandler("localhost:7233") tests := []struct { name string request RequestPayload expectedStatus int shouldFail bool }{ { name: "Missing workflow_id", request: RequestPayload{ Action: "START_WORKFLOW", Namespace: "default", Payload: map[string]interface{}{ "workflow_type": "Test", "task_queue": "default", }, }, expectedStatus: http.StatusOK, // Handler returns success even if fields missing shouldFail: true, }, { name: "Missing signal_name", request: RequestPayload{ Action: "SIGNAL_WORKFLOW", Namespace: "default", Payload: map[string]interface{}{ "workflow_id": "test", }, }, expectedStatus: http.StatusOK, shouldFail: true, }, { name: "Empty namespace", request: RequestPayload{ Action: "DESCRIBE_WORKFLOW", Namespace: "", Payload: map[string]interface{}{ "workflow_id": "test", }, }, expectedStatus: http.StatusOK, shouldFail: false, // Should default to "default" }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { body, _ := json.Marshal(test.request) req := httptest.NewRequest("POST", "/workflow", bytes.NewReader(body)) w := httptest.NewRecorder() handler.handleWorkflow(w, req) var resp ResponsePayload json.NewDecoder(w.Body).Decode(&resp) if test.shouldFail && resp.Success { t.Errorf("Expected failure for %s", test.name) } if test.request.Namespace == "" && resp.Namespace != "default" { t.Errorf("Expected namespace to default to 'default', got %s", resp.Namespace) } }) } } // TestIntegration_ResponseTimestamp verifies timestamp accuracy func TestIntegration_ResponseTimestamp(t *testing.T) { if !isTemporalAvailable() { t.Skip("Temporal server not available"); return } handler := NewHandler("localhost:7233") before := time.Now() req := RequestPayload{ Action: "DESCRIBE_WORKFLOW", Namespace: "default", Payload: map[string]interface{}{ "workflow_id": "test", }, } body, _ := json.Marshal(req) httpReq := httptest.NewRequest("POST", "/workflow", bytes.NewReader(body)) w := httptest.NewRecorder() handler.handleWorkflow(w, httpReq) after := time.Now() var resp ResponsePayload json.NewDecoder(w.Body).Decode(&resp) if resp.Timestamp.IsZero() { t.Fatal("Timestamp is zero") } if resp.Timestamp.Before(before) || resp.Timestamp.After(after) { t.Errorf("Timestamp not within expected range. Response: %v, Before: %v, After: %v", resp.Timestamp, before, after) } } // TestIntegration_AllOperationsWithValidInput tests all operations with minimal valid input func TestIntegration_AllOperationsWithValidInput(t *testing.T) { if !isTemporalAvailable() { t.Skip("Temporal server not available"); return } handler := NewHandler("localhost:7233") operations := []struct { name string action string payload map[string]interface{} }{ {"START_WORKFLOW", "START_WORKFLOW", map[string]interface{}{"workflow_id": "test", "workflow_type": "T", "task_queue": "q"}}, {"DESCRIBE_WORKFLOW", "DESCRIBE_WORKFLOW", map[string]interface{}{"workflow_id": "test"}}, {"LIST_WORKFLOWS", "LIST_WORKFLOWS", map[string]interface{}{}}, {"GET_WORKFLOW_HISTORY", "GET_WORKFLOW_HISTORY", map[string]interface{}{"workflow_id": "test"}}, {"TERMINATE_WORKFLOW", "TERMINATE_WORKFLOW", map[string]interface{}{"workflow_id": "test"}}, {"CANCEL_WORKFLOW", "CANCEL_WORKFLOW", map[string]interface{}{"workflow_id": "test"}}, {"SIGNAL_WORKFLOW", "SIGNAL_WORKFLOW", map[string]interface{}{"workflow_id": "test", "signal_name": "sig"}}, {"QUERY_WORKFLOW", "QUERY_WORKFLOW", map[string]interface{}{"workflow_id": "test", "query_type": "q"}}, {"RESET_WORKFLOW", "RESET_WORKFLOW", map[string]interface{}{"workflow_id": "test", "reset_type": "t"}}, {"UPDATE_WORKFLOW", "UPDATE_WORKFLOW", map[string]interface{}{"workflow_id": "test", "update_name": "u"}}, {"HEARTBEAT_ACTIVITY", "HEARTBEAT_ACTIVITY", map[string]interface{}{"task_token": "t"}}, {"COMPLETE_ACTIVITY", "COMPLETE_ACTIVITY", map[string]interface{}{"task_token": "t"}}, {"FAIL_ACTIVITY", "FAIL_ACTIVITY", map[string]interface{}{"task_token": "t"}}, {"LIST_NAMESPACES", "LIST_NAMESPACES", map[string]interface{}{}}, {"DESCRIBE_NAMESPACE", "DESCRIBE_NAMESPACE", map[string]interface{}{}}, {"CREATE_NAMESPACE", "CREATE_NAMESPACE", map[string]interface{}{"namespace_name": "test"}}, {"UPDATE_NAMESPACE", "UPDATE_NAMESPACE", map[string]interface{}{}}, {"DELETE_NAMESPACE", "DELETE_NAMESPACE", map[string]interface{}{}}, {"LIST_SEARCH_ATTRIBUTES", "LIST_SEARCH_ATTRIBUTES", map[string]interface{}{}}, {"ADD_SEARCH_ATTRIBUTES", "ADD_SEARCH_ATTRIBUTES", map[string]interface{}{"search_attributes": map[string]interface{}{"attr1": "value1"}}}, {"LIST_TASK_QUEUES", "LIST_TASK_QUEUES", map[string]interface{}{}}, {"GET_CLUSTER_INFO", "GET_CLUSTER_INFO", map[string]interface{}{}}, {"LIST_CLUSTER_MEMBERS", "LIST_CLUSTER_MEMBERS", map[string]interface{}{}}, {"GET_SYSTEM_INFO", "GET_SYSTEM_INFO", map[string]interface{}{}}, } for _, op := range operations { t.Run(op.name, func(t *testing.T) { req := RequestPayload{ Action: op.action, Namespace: "default", Payload: op.payload, } body, _ := json.Marshal(req) httpReq := httptest.NewRequest("POST", "/workflow", bytes.NewReader(body)) w := httptest.NewRecorder() handler.handleWorkflow(w, httpReq) if w.Code != http.StatusOK { t.Errorf("Operation %s failed with status %d", op.action, w.Code) } var resp ResponsePayload json.NewDecoder(w.Body).Decode(&resp) if resp.Action != op.action { t.Errorf("Expected action %s, got %s", op.action, resp.Action) } if resp.Timestamp.IsZero() { t.Errorf("Timestamp not set for %s", op.action) } }) } }