From da3015c72e180a65931990ba33f71ab6f199f270 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Tue, 12 May 2026 08:18:30 +0000 Subject: [PATCH 1/2] =?UTF-8?q?test(handlers/bundle):=20add=20bundle=5Ftes?= =?UTF-8?q?t.go=20=E2=80=94=205=20cases=20covering=20Import=20+=20Export?= =?UTF-8?q?=20error=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers: - BundleHandler.Import: invalid JSON (7 sub-cases) → 400 - BundleHandler.Import: valid JSON → 201 - BundleHandler.Export: workspace not found (ErrNoRows) → 404 - BundleHandler.Export: DB query error → 404 Branch: feat/workspace-dispatchers-test-coverage Co-Authored-By: Claude Opus 4.7 --- .../internal/handlers/bundle_test.go | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 workspace-server/internal/handlers/bundle_test.go diff --git a/workspace-server/internal/handlers/bundle_test.go b/workspace-server/internal/handlers/bundle_test.go new file mode 100644 index 00000000..27706b61 --- /dev/null +++ b/workspace-server/internal/handlers/bundle_test.go @@ -0,0 +1,129 @@ +package handlers + +import ( + "bytes" + "database/sql" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" +) + +// ───────────────────────────────────────────────────────────────────────────── +// BundleHandler Import — JSON binding error cases +// ───────────────────────────────────────────────────────────────────────────── + +func TestBundleImport_InvalidJSON(t *testing.T) { + h := NewBundleHandler(nil, nil, "http://localhost:8080", t.TempDir(), nil) + + tests := []struct { + name string + body string + }{ + {"not JSON", `not json at all`}, + {"truncated JSON", `{"name": "test",`}, + {"null", `null`}, + {"array", `[]`}, + {"number", `42`}, + {"boolean", `true`}, + {"string", `"just a string"`}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest("POST", "/bundles/import", bytes.NewBufferString(tc.body)) + c.Request.Header.Set("Content-Type", "application/json") + + h.Import(c) + + if w.Code != http.StatusBadRequest { + t.Errorf("invalid JSON %q: expected status %d, got %d", tc.body, http.StatusBadRequest, w.Code) + } + }) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// BundleHandler Import — valid JSON routes to bundle.Import and returns 201 +// ───────────────────────────────────────────────────────────────────────────── + +func TestBundleImport_ValidJSON(t *testing.T) { + h := NewBundleHandler(nil, nil, "http://localhost:8080", t.TempDir(), nil) + + body := `{"name": "test-workspace", "schema": "1.0", "tier": 3}` + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest("POST", "/bundles/import", bytes.NewBufferString(body)) + c.Request.Header.Set("Content-Type", "application/json") + + h.Import(c) + + if w.Code != http.StatusCreated { + t.Errorf("valid JSON: expected status %d, got %d: %s", http.StatusCreated, w.Code, w.Body.String()) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// BundleHandler Export — workspace not found (ErrNoRows → 404) +// ───────────────────────────────────────────────────────────────────────────── + +func TestBundleExport_NotFound(t *testing.T) { + mock := setupTestDB(t) + _ = setupTestRedis(t) + broadcaster := newTestBroadcaster() + h := NewBundleHandler(broadcaster, nil, "http://localhost:8080", t.TempDir(), nil) + + // bundle.Export queries the workspace row — return ErrNoRows for missing workspace. + mock.ExpectQuery(`SELECT name, COALESCE\(role`). + WithArgs("ws-nonexistent"). + WillReturnError(sql.ErrNoRows) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "ws-nonexistent"}} + c.Request = httptest.NewRequest("GET", "/bundles/export/ws-nonexistent", nil) + + h.Export(c) + + if w.Code != http.StatusNotFound { + t.Errorf("expected status %d, got %d: %s", http.StatusNotFound, w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet sqlmock expectations: %v", err) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// BundleHandler Export — query error (DB error → 404, per bundle.Export semantics) +// ───────────────────────────────────────────────────────────────────────────── + +func TestBundleExport_QueryError(t *testing.T) { + mock := setupTestDB(t) + _ = setupTestRedis(t) + broadcaster := newTestBroadcaster() + h := NewBundleHandler(broadcaster, nil, "http://localhost:8080", t.TempDir(), nil) + + // Simulate a non-ErrNoRows DB error. + mock.ExpectQuery(`SELECT name, COALESCE\(role`). + WithArgs("ws-error"). + WillReturnError(sql.ErrConnDone) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "ws-error"}} + c.Request = httptest.NewRequest("GET", "/bundles/export/ws-error", nil) + + h.Export(c) + + // bundle.Export wraps DB errors as "failed to fetch workspace" which is not + // "workspace not found", but the handler maps any error → 404 for Export. + if w.Code != http.StatusNotFound { + t.Errorf("expected status %d for DB error, got %d: %s", http.StatusNotFound, w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet sqlmock expectations: %v", err) + } +} -- 2.45.2 From 0d74b1fa7943c2792bc87255800cc414bbdd5377 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Wed, 13 May 2026 05:37:43 +0000 Subject: [PATCH 2/2] [core-be-agent] fix(bundle_test): TestBundleImport_ValidJSON nil broadcaster panic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestBundleImport_ValidJSON passed nil broadcaster to BundleHandler. bundle.Import calls broadcaster.RecordAndBroadcast unconditionally → panic when broadcaster is nil. Fix: add setupTestDB + newTestBroadcaster + 4 ExpectExec mocks covering the INSERT workspaces / UPDATE runtime / INSERT schedules / INSERT workspace_secrets calls. Recursive sub-workspace imports are not triggered (bundle has no SubWorkspaces), and prov is nil so the provision goroutine + markFailed are not reached. Also caught: the original test never called setupTestDB, so db.DB was uninitialized (nil) and the first INSERT would have panicked with "nil pointer" before reaching the broadcaster panic. --- .../internal/handlers/bundle_test.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/workspace-server/internal/handlers/bundle_test.go b/workspace-server/internal/handlers/bundle_test.go index 27706b61..0494e22e 100644 --- a/workspace-server/internal/handlers/bundle_test.go +++ b/workspace-server/internal/handlers/bundle_test.go @@ -51,7 +51,20 @@ func TestBundleImport_InvalidJSON(t *testing.T) { // ───────────────────────────────────────────────────────────────────────────── func TestBundleImport_ValidJSON(t *testing.T) { - h := NewBundleHandler(nil, nil, "http://localhost:8080", t.TempDir(), nil) + mock := setupTestDB(t) + broadcaster := newTestBroadcaster() + h := NewBundleHandler(broadcaster, nil, "http://localhost:8080", t.TempDir(), nil) + + // bundle.Import does: INSERT workspaces, UPDATE runtime, INSERT schedules, INSERT secrets. + // bundle.Import recurses into SubWorkspaces (empty in this test bundle → no recursive INSERTs). + mock.ExpectExec("INSERT INTO workspaces"). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectExec("UPDATE workspaces SET runtime"). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectExec("INSERT INTO workspace_schedules"). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectExec("INSERT INTO workspace_secrets"). + WillReturnResult(sqlmock.NewResult(0, 1)) body := `{"name": "test-workspace", "schema": "1.0", "tier": 3}` w := httptest.NewRecorder() @@ -64,6 +77,9 @@ func TestBundleImport_ValidJSON(t *testing.T) { if w.Code != http.StatusCreated { t.Errorf("valid JSON: expected status %d, got %d: %s", http.StatusCreated, w.Code, w.Body.String()) } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet sqlmock expectations: %v", err) + } } // ───────────────────────────────────────────────────────────────────────────── -- 2.45.2