All checks were successful
sop-checklist / all-items-acked (pull_request) All SOP items acked
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 33s
CI / Detect changes (pull_request) Successful in 1m52s
Harness Replays / detect-changes (pull_request) Successful in 26s
E2E API Smoke Test / detect-changes (pull_request) Successful in 1m51s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 1m45s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 1m48s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 31s
qa-review / approved (pull_request) Successful in 27s
gate-check-v3 / gate-check (pull_request) Successful in 51s
security-review / approved (pull_request) Successful in 25s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 1m20s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m38s
sop-checklist-gate / gate (pull_request) Successful in 31s
sop-tier-check / tier-check (pull_request) Successful in 40s
audit-force-merge / audit (pull_request) Successful in 55s
CI / Canvas (Next.js) (pull_request) Successful in 20s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 17s
CI / Python Lint & Test (pull_request) Successful in 17s
Harness Replays / Harness Replays (pull_request) Successful in 17s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 29s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 19s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 3m12s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 6m24s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Successful in 20m6s
CI / all-required (pull_request) Successful in 5s
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/DATA-DOG/go-sqlmock"
|
|
"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)
|
|
_ = setupTestRedis(t)
|
|
broadcaster := newTestBroadcaster()
|
|
h := NewBundleHandler(broadcaster, nil, "http://localhost:8080", t.TempDir(), nil)
|
|
|
|
// bundle.Import does: INSERT workspaces, broadcast provisioning, then UPDATE runtime.
|
|
// 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("INSERT INTO structure_events").
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectExec("UPDATE workspaces SET runtime").
|
|
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)
|
|
}
|
|
}
|