diff --git a/workspace-server/internal/handlers/approvals.go b/workspace-server/internal/handlers/approvals.go index 1f091afa3..c0543e726 100644 --- a/workspace-server/internal/handlers/approvals.go +++ b/workspace-server/internal/handlers/approvals.go @@ -34,13 +34,19 @@ func (h *ApprovalsHandler) Create(c *gin.Context) { return } - ctxJSON, _ := json.Marshal(body.Context) - if ctxJSON == nil { + ctxJSON, err := json.Marshal(body.Context) + if err != nil { + log.Printf("Create approval: json.Marshal(context) error: %v", err) + ctxJSON = []byte("{}") + } else if len(ctxJSON) == 0 { + // json.Marshal returns []byte{} (empty slice, not nil) on error; + // guard against it defensively even though map[string]interface{} + // cannot fail in practice — defensive in depth. ctxJSON = []byte("{}") } var approvalID string - err := db.DB.QueryRowContext(ctx, ` + err = db.DB.QueryRowContext(ctx, ` INSERT INTO approval_requests (workspace_id, task_id, action, reason, context) VALUES ($1, $2, $3, $4, $5::jsonb) RETURNING id diff --git a/workspace-server/internal/handlers/approvals_test.go b/workspace-server/internal/handlers/approvals_test.go index e2fb30814..a294a080b 100644 --- a/workspace-server/internal/handlers/approvals_test.go +++ b/workspace-server/internal/handlers/approvals_test.go @@ -328,3 +328,35 @@ func TestApprovals_Decide_MissingDecision(t *testing.T) { t.Errorf("expected 400, got %d", w.Code) } } + +func TestApprovals_Create_NilContextFallsBackToEmptyJSON(t *testing.T) { + mock := setupTestDB(t) + setupTestRedis(t) + broadcaster := newTestBroadcaster() + handler := NewApprovalsHandler(broadcaster) + + mock.ExpectQuery("INSERT INTO approval_requests"). + WithArgs("ws-1", "task-0", "approve", "none", "{}", sqlmock.AnyArg()). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("appr-nil")) + + mock.ExpectExec("INSERT INTO structure_events"). + WillReturnResult(sqlmock.NewResult(0, 1)) + + mock.ExpectQuery("SELECT parent_id FROM workspaces WHERE id"). + WithArgs("ws-1"). + WillReturnRows(sqlmock.NewRows([]string{"parent_id"}).AddRow(nil)) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "ws-1"}} + // context is nil (zero value of map[string]interface{}) + body := `{"action":"approve","reason":"none","task_id":"task-0","context":null}` + c.Request = httptest.NewRequest("POST", "/", bytes.NewBufferString(body)) + c.Request.Header.Set("Content-Type", "application/json") + + handler.Create(c) + + if w.Code != http.StatusCreated { + t.Errorf("expected 201, got %d: %s", w.Code, w.Body.String()) + } +}