Some checks failed
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 5s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 11s
CI / Detect changes (pull_request) Successful in 19s
publish-runtime-autobump / bump-and-tag (pull_request) Has been skipped
Harness Replays / detect-changes (pull_request) Successful in 22s
E2E API Smoke Test / detect-changes (pull_request) Successful in 25s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 29s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 29s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 38s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 20s
sop-checklist / all-items-acked (pull_request) acked: 7/7
security-review / approved (pull_request) Failing after 18s
qa-review / approved (pull_request) Failing after 18s
sop-checklist-gate / gate (pull_request) Successful in 19s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 30s
gate-check-v3 / gate-check (pull_request) Successful in 29s
sop-tier-check / tier-check (pull_request) Successful in 14s
publish-runtime-autobump / pr-validate (pull_request) Successful in 41s
Harness Replays / Harness Replays (pull_request) Successful in 8s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 19s
MCP Stdio Transport Regression / MCP stdio with regular-file stdout (pull_request) Successful in 1m13s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m16s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m37s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m28s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m51s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 2m19s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m49s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 2m18s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 3m45s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m25s
CI / Python Lint & Test (pull_request) Successful in 7m30s
CI / Platform (Go) (pull_request) Failing after 8m14s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 11m11s
CI / Canvas (Next.js) (pull_request) Failing after 14m35s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Failing after 4s
1. ci-mcp-stdio-transport.yml: install pytest-cov so --no-cov flag doesn't conflict with workspace/pytest.ini addopts (exit code 4). Run 26124 (MCP stdio with regular-file stdout). 2. ci-mcp-stdio-transport.yml: add # mc#774 tracker on continue-on-error: true to satisfy lint-continue-on-error-tracking Tier 2e. Run 26132. 3. ci-mcp-stdio-transport.yml: add # bp-exempt directive comment above mcp-stdio-regular-file job key to satisfy lint-required-context-exists-in-bp Tier 2g. Run 26135. 4. bundle_test.go: import github.com/DATA-DOG/go-sqlmock explicitly so the package identifier resolves when compiled with -tags=integration. Run 26130 (Handlers Postgres Integration). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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, 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("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)
|
|
}
|
|
}
|