Some checks failed
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 3s
Harness Replays / detect-changes (pull_request) Successful in 7s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 9s
qa-review / approved (pull_request) Failing after 11s
security-review / approved (pull_request) Failing after 11s
CI / Detect changes (pull_request) Successful in 16s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 17s
Harness Replays / Harness Replays (pull_request) Successful in 5s
E2E API Smoke Test / detect-changes (pull_request) Successful in 18s
gate-check-v3 / gate-check (pull_request) Successful in 18s
sop-checklist-gate / gate (pull_request) Successful in 10s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 20s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 20s
sop-tier-check / tier-check (pull_request) Successful in 10s
CI / Canvas (Next.js) (pull_request) Successful in 4s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 3s
CI / Python Lint & Test (pull_request) Successful in 3s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 4s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 5s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m4s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 59s
CI / Platform (Go) (pull_request) Failing after 2m1s
CI / all-required (pull_request) Successful in 1s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 2m3s
sop-checklist / all-items-acked (pull_request) tier:low compensating success — test-only addition (bundle_test.go), no functional change
audit-force-merge / audit (pull_request) Successful in 14s
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.
146 lines
6.1 KiB
Go
146 lines
6.1 KiB
Go
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) {
|
|
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()
|
|
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())
|
|
}
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("unmet sqlmock expectations: %v", err)
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 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)
|
|
}
|
|
}
|