package api import ( "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gorilla/websocket" ) // TestComplete_ErrorIsJSONString ensures task_complete encodes the failure // reason as a string. Marshaling a Go error interface produces {}, which the // manager cannot unmarshal into WSTaskUpdate.Error and used to look like a // silent WebSocket drop with no retries. func TestComplete_ErrorIsJSONString(t *testing.T) { upgrader := websocket.Upgrader{} received := make(chan map[string]interface{}, 1) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return } defer conn.Close() // auth handshake var auth map[string]interface{} if err := conn.ReadJSON(&auth); err != nil { return } if auth["type"] == "auth" { _ = conn.WriteJSON(map[string]string{"type": "auth_ok"}) } var msg map[string]interface{} if err := conn.ReadJSON(&msg); err != nil { return } received <- msg })) defer server.Close() jc := NewJobConnection() if err := jc.Connect(server.URL, "/job/1", "token123"); err != nil { t.Fatalf("Connect failed: %v", err) } defer jc.Close() jc.Complete(42, false, errors.New("blender failed: signal: segmentation fault (core dumped)"), true) select { case msg := <-received: if msg["type"] != "task_complete" { t.Fatalf("type = %v, want task_complete", msg["type"]) } data, ok := msg["data"].(map[string]interface{}) if !ok { // gorilla may leave nested objects as map[string]interface{} after JSON round-trip; // also accept raw re-marshal path raw, _ := json.Marshal(msg["data"]) data = map[string]interface{}{} if err := json.Unmarshal(raw, &data); err != nil { t.Fatalf("data type %T: %v", msg["data"], err) } } errVal, ok := data["error"].(string) if !ok { t.Fatalf("error field type %T value %#v; want string (not object)", data["error"], data["error"]) } if errVal != "blender failed: signal: segmentation fault (core dumped)" { t.Fatalf("error = %q, want segfault message", errVal) } if data["success"] != false { t.Fatalf("success = %v, want false", data["success"]) } if data["free_requeue"] != true { t.Fatalf("free_requeue = %v, want true", data["free_requeue"]) } case <-time.After(2 * time.Second): t.Fatal("timed out waiting for task_complete message") } } func TestJobConnection_ConnectAndClose(t *testing.T) { upgrader := websocket.Upgrader{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return } defer conn.Close() var msg map[string]interface{} if err := conn.ReadJSON(&msg); err != nil { return } if msg["type"] == "auth" { _ = conn.WriteJSON(map[string]string{"type": "auth_ok"}) } // Keep open briefly so client can mark connected. time.Sleep(100 * time.Millisecond) })) defer server.Close() jc := NewJobConnection() managerURL := strings.Replace(server.URL, "http://", "http://", 1) if err := jc.Connect(managerURL, "/job/1", "token123"); err != nil { t.Fatalf("Connect failed: %v", err) } if !jc.IsConnected() { t.Fatal("expected connection to be marked connected") } jc.Close() }