molecule-core/platform/internal/handlers/discovery_test.go
Hongming Wang d751420679 test: 100% coverage of extracted helpers + ConfirmDialog singleButton
Follow-up to the quality-fixes-pass2 code review.

## Go: direct unit tests for PR #5 extracted helpers (~47 new tests)

a2a_proxy_test.go:
- resolveAgentURL: cache hit, cache-miss DB hit, not-found, null-URL,
  docker-rewrite guard
- dispatchA2A: build error, canvas timeout, agent timeout, success
- handleA2ADispatchError: context deadline, generic error, build error
- maybeMarkContainerDead: nil-provisioner, runtime=external short-circuits
- logA2AFailure, logA2ASuccess: activity_logs row content + status

delegation_test.go:
- bindDelegateRequest: valid / malformed / bad-UUID
- lookupIdempotentDelegation: no-key / no-match / failed-row-deleted / existing-pending
- insertDelegationRow: insertOK / insertHandledByIdempotent /
  insertTrackingUnavailable
- insertDelegationOutcome: zero-value is insertOutcomeUnknown sentinel

discovery_test.go:
- discoverWorkspacePeer: online / not-found / access-denied + 2 edges
- writeExternalWorkspaceURL: 3 cases
- discoverHostPeer: smoke test documents the unreachable-by-design path

activity_test.go:
- parseSessionSearchParams: defaults + custom limit/offset/q
- buildSessionSearchQuery: no-filters + with-query shapes
- scanSessionSearchRows: empty / single / multiple rows

Package coverage: 56.1% → 57.6%. Every helper extracted in PR #5 is
now at or near 100% line coverage (see PR notes for the 4 remaining
gaps, all blocked on provisioner interface mockability).

## Defensive enum zero-value fix

insertDelegationOutcome now starts with insertOutcomeUnknown=0 as a
sentinel so an un-initialized variable can't silently read as
"success". insertOK, insertHandledByIdempotent, insertTrackingUnavailable
shift to 1/2/3. No caller changes needed.

## Canvas: ConfirmDialog.singleButton test (5 cases)

canvas/src/components/__tests__/ConfirmDialog.test.tsx covers:
- default render (both buttons)
- singleButton hides Cancel
- singleButton: Escape still fires onCancel
- singleButton: backdrop-click still fires onCancel
- singleButton: onConfirm fires on click

vitest total: 352 → 357, all passing.

## Docstring clarity

ConfirmDialog.tsx: expanded singleButton prop comment to explicitly
instruct callers to pass the same handler for onConfirm/onCancel when
using it as an info toast (matches TemplatePalette usage).

## ErrorBoundary clipboard observability

.catch(() => {}) silently swallowed rejections. Now:
.catch((e) => console.warn("clipboard write failed:", e))
so permission-denied / insecure-context failures surface in the console.

## Verification

- go build ./... clean
- go vet ./... clean
- go test -race ./internal/... — all pass
- canvas npm run build — clean
- canvas npm test -- --run — 357/357 pass
- tests/e2e/test_api.sh — 46/62 pass; all 16 failures are pre-existing
  (token-auth enforcement + stale test workspaces + missing Docker
  network). None involve handlers touched in PR #5.
- Manual: platform + canvas running locally, title=Molecule AI,
  /workspaces returns [], /health returns ok. Identified + killed a
  stale Next.js server from the old Starfire-AgentTeam repo that was
  serving the old brand on IPv4 port 3000.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:08:33 -07:00

621 lines
20 KiB
Go

package handlers
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
)
// ==================== Discover — missing X-Workspace-ID header ====================
func TestDiscover_MissingCallerHeader(t *testing.T) {
setupTestDB(t)
setupTestRedis(t)
handler := NewDiscoveryHandler()
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "ws-target"}}
c.Request = httptest.NewRequest("GET", "/registry/discover/ws-target", nil)
// No X-Workspace-ID header
handler.Discover(c)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if resp["error"] != "X-Workspace-ID header is required" {
t.Errorf("expected error about missing header, got %v", resp["error"])
}
}
// ==================== Discover — workspace not found (with caller) ====================
func TestDiscover_WorkspaceNotFound_WithCaller(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
handler := NewDiscoveryHandler()
// CanCommunicate will need DB lookups — both workspace name lookups
// For the access check: caller lookup succeeds, target lookup fails
mock.ExpectQuery("SELECT id, parent_id FROM workspaces WHERE id =").
WithArgs("ws-caller").
WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-caller", nil))
mock.ExpectQuery("SELECT id, parent_id FROM workspaces WHERE id =").
WithArgs("ws-missing").
WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"})) // no rows
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "ws-missing"}}
c.Request = httptest.NewRequest("GET", "/registry/discover/ws-missing", nil)
c.Request.Header.Set("X-Workspace-ID", "ws-caller")
handler.Discover(c)
// Access denied because target not found in registry → 403
if w.Code != http.StatusForbidden {
t.Errorf("expected status 403, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
// ==================== Discover — external (no caller header, DB fallback) ====================
func TestDiscover_External_NotFound(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
handler := NewDiscoveryHandler()
// This tests the external path (no X-Workspace-ID header), but we need
// the request to have the header as empty string bypass. Instead test the
// DB path for external callers:
// For an external request without caller, the code first checks callerID == ""
// which triggers the StatusBadRequest, so we test with a header but Redis+DB miss
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "ws-ext-missing"}}
c.Request = httptest.NewRequest("GET", "/registry/discover/ws-ext-missing", nil)
// No header → returns 400
handler.Discover(c)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
// ==================== Peers — success with parent/siblings/children ====================
func TestPeers_WithParent(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
handler := NewDiscoveryHandler()
// Expect parent_id lookup for the requesting workspace
mock.ExpectQuery("SELECT parent_id FROM workspaces WHERE id =").
WithArgs("ws-sibling-1").
WillReturnRows(sqlmock.NewRows([]string{"parent_id"}).AddRow("ws-parent"))
// Expect siblings query (same parent, excluding self)
peerCols := []string{"id", "name", "role", "tier", "status", "agent_card", "url", "parent_id", "active_tasks"}
mock.ExpectQuery("SELECT w.id, w.name.*WHERE w.parent_id = \\$1 AND w.id != \\$2").
WithArgs("ws-parent", "ws-sibling-1").
WillReturnRows(sqlmock.NewRows(peerCols).
AddRow("ws-sibling-2", "Sibling Two", "worker", 1, "online", []byte("null"), "http://localhost:8002", "ws-parent", 0))
// Expect children query
mock.ExpectQuery("SELECT w.id, w.name.*WHERE w.parent_id = \\$1 AND w.status").
WithArgs("ws-sibling-1").
WillReturnRows(sqlmock.NewRows(peerCols))
// Expect parent query
mock.ExpectQuery("SELECT w.id, w.name.*WHERE w.id = \\$1 AND w.status").
WithArgs("ws-parent").
WillReturnRows(sqlmock.NewRows(peerCols).
AddRow("ws-parent", "Parent PM", "manager", 2, "online", []byte("null"), "http://localhost:8001", nil, 1))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "ws-sibling-1"}}
c.Request = httptest.NewRequest("GET", "/registry/ws-sibling-1/peers", nil)
handler.Peers(c)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
var peers []map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &peers); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(peers) != 2 {
t.Errorf("expected 2 peers (1 sibling + 1 parent), got %d", len(peers))
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
func TestPeers_NotFound(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
handler := NewDiscoveryHandler()
// Workspace not found
mock.ExpectQuery("SELECT parent_id FROM workspaces WHERE id =").
WithArgs("ws-ghost").
WillReturnError(sql.ErrNoRows)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "ws-ghost"}}
c.Request = httptest.NewRequest("GET", "/registry/ws-ghost/peers", nil)
handler.Peers(c)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
func TestPeers_DBError(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
handler := NewDiscoveryHandler()
mock.ExpectQuery("SELECT parent_id FROM workspaces WHERE id =").
WithArgs("ws-dberr").
WillReturnError(sql.ErrConnDone)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "ws-dberr"}}
c.Request = httptest.NewRequest("GET", "/registry/ws-dberr/peers", nil)
handler.Peers(c)
if w.Code != http.StatusInternalServerError {
t.Errorf("expected status 500, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
func TestPeers_RootWorkspace_NoPeers(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
handler := NewDiscoveryHandler()
// Root workspace (parent_id is NULL)
mock.ExpectQuery("SELECT parent_id FROM workspaces WHERE id =").
WithArgs("ws-root-alone").
WillReturnRows(sqlmock.NewRows([]string{"parent_id"}).AddRow(nil))
peerCols := []string{"id", "name", "role", "tier", "status", "agent_card", "url", "parent_id", "active_tasks"}
// Siblings (other root-level workspaces) — none
mock.ExpectQuery("SELECT w.id, w.name.*WHERE w.parent_id IS NULL AND w.id != \\$1").
WithArgs("ws-root-alone").
WillReturnRows(sqlmock.NewRows(peerCols))
// Children — none
mock.ExpectQuery("SELECT w.id, w.name.*WHERE w.parent_id = \\$1").
WithArgs("ws-root-alone").
WillReturnRows(sqlmock.NewRows(peerCols))
// No parent query since parent_id is NULL
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "ws-root-alone"}}
c.Request = httptest.NewRequest("GET", "/registry/ws-root-alone/peers", nil)
handler.Peers(c)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
var peers []map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &peers); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(peers) != 0 {
t.Errorf("expected 0 peers, got %d", len(peers))
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
// ==================== CheckAccess ====================
func TestCheckAccess_BadJSON(t *testing.T) {
setupTestDB(t)
setupTestRedis(t)
handler := NewDiscoveryHandler()
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("POST", "/registry/check-access", bytes.NewBufferString("not json"))
c.Request.Header.Set("Content-Type", "application/json")
handler.CheckAccess(c)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String())
}
}
func TestCheckAccess_MissingFields(t *testing.T) {
setupTestDB(t)
setupTestRedis(t)
handler := NewDiscoveryHandler()
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"caller_id":"ws-1"}`
c.Request = httptest.NewRequest("POST", "/registry/check-access", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.CheckAccess(c)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String())
}
}
func TestCheckAccess_SameWorkspace(t *testing.T) {
setupTestDB(t)
setupTestRedis(t)
handler := NewDiscoveryHandler()
// CanCommunicate("ws-1", "ws-1") returns true immediately (same ID, no DB lookups)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"caller_id":"ws-1","target_id":"ws-1"}`
c.Request = httptest.NewRequest("POST", "/registry/check-access", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.CheckAccess(c)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if resp["allowed"] != true {
t.Errorf("expected allowed=true for same workspace, got %v", resp["allowed"])
}
}
// ==================== Direct unit tests for extracted helpers ====================
// --- discoverWorkspacePeer ---
func TestDiscoverWorkspacePeer_Online(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
// name/runtime lookup → non-external
mock.ExpectQuery(`SELECT COALESCE\(name,''\), COALESCE\(runtime,'langgraph'\) FROM workspaces WHERE id =`).
WithArgs("ws-online").
WillReturnRows(sqlmock.NewRows([]string{"name", "runtime"}).AddRow("Target", "langgraph"))
// No cached internal URL → DB status lookup → online
mock.ExpectQuery(`SELECT status FROM workspaces WHERE id =`).
WithArgs("ws-online").
WillReturnRows(sqlmock.NewRows([]string{"status"}).AddRow("online"))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/x", nil)
discoverWorkspacePeer(context.Background(), c, "ws-caller", "ws-online")
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
if resp["id"] != "ws-online" || resp["url"] == "" {
t.Errorf("unexpected body: %v", resp)
}
}
func TestDiscoverWorkspacePeer_NotFound(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
mock.ExpectQuery(`SELECT COALESCE\(name,''\), COALESCE\(runtime,'langgraph'\) FROM workspaces WHERE id =`).
WithArgs("ws-missing").
WillReturnRows(sqlmock.NewRows([]string{"name", "runtime"}).AddRow("", "langgraph"))
mock.ExpectQuery(`SELECT status FROM workspaces WHERE id =`).
WithArgs("ws-missing").
WillReturnError(sql.ErrNoRows)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/x", nil)
discoverWorkspacePeer(context.Background(), c, "ws-caller", "ws-missing")
if w.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String())
}
}
func TestDiscoverWorkspacePeer_ExternalRuntime_HandledByExternalURL(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
mock.ExpectQuery(`SELECT COALESCE\(name,''\), COALESCE\(runtime,'langgraph'\) FROM workspaces WHERE id =`).
WithArgs("ws-ext").
WillReturnRows(sqlmock.NewRows([]string{"name", "runtime"}).AddRow("Ext", "external"))
// writeExternalWorkspaceURL's two queries
mock.ExpectQuery(`SELECT COALESCE\(url,''\) FROM workspaces WHERE id =`).
WithArgs("ws-ext").
WillReturnRows(sqlmock.NewRows([]string{"url"}).AddRow("http://external.example"))
mock.ExpectQuery(`SELECT COALESCE\(runtime,'langgraph'\) FROM workspaces WHERE id =`).
WithArgs("ws-caller").
WillReturnRows(sqlmock.NewRows([]string{"runtime"}).AddRow("external"))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/x", nil)
discoverWorkspacePeer(context.Background(), c, "ws-caller", "ws-ext")
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestDiscoverWorkspacePeer_CachedInternalURLHit(t *testing.T) {
mock := setupTestDB(t)
mr := setupTestRedis(t)
mock.ExpectQuery(`SELECT COALESCE\(name,''\), COALESCE\(runtime,'langgraph'\) FROM workspaces WHERE id =`).
WithArgs("ws-cached").
WillReturnRows(sqlmock.NewRows([]string{"name", "runtime"}).AddRow("Cached", "langgraph"))
mr.Set("ws:ws-cached:internal_url", "http://ws-cached:8000")
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/x", nil)
discoverWorkspacePeer(context.Background(), c, "ws-caller", "ws-cached")
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
if resp["url"] != "http://ws-cached:8000" {
t.Errorf("expected cached internal URL, got %v", resp["url"])
}
}
func TestDiscoverWorkspacePeer_NotReachable(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
mock.ExpectQuery(`SELECT COALESCE\(name,''\), COALESCE\(runtime,'langgraph'\) FROM workspaces WHERE id =`).
WithArgs("ws-paused").
WillReturnRows(sqlmock.NewRows([]string{"name", "runtime"}).AddRow("Paused", "langgraph"))
mock.ExpectQuery(`SELECT status FROM workspaces WHERE id =`).
WithArgs("ws-paused").
WillReturnRows(sqlmock.NewRows([]string{"status"}).AddRow("paused"))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/x", nil)
discoverWorkspacePeer(context.Background(), c, "ws-caller", "ws-paused")
if w.Code != http.StatusServiceUnavailable {
t.Errorf("expected 503, got %d: %s", w.Code, w.Body.String())
}
}
// --- writeExternalWorkspaceURL ---
func TestWriteExternalWorkspaceURL_Success(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
mock.ExpectQuery(`SELECT COALESCE\(url,''\) FROM workspaces WHERE id =`).
WithArgs("ws-ext").
WillReturnRows(sqlmock.NewRows([]string{"url"}).AddRow("http://external.example/a2a"))
mock.ExpectQuery(`SELECT COALESCE\(runtime,'langgraph'\) FROM workspaces WHERE id =`).
WithArgs("ws-caller").
WillReturnRows(sqlmock.NewRows([]string{"runtime"}).AddRow("langgraph"))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/x", nil)
handled := writeExternalWorkspaceURL(context.Background(), c, "ws-caller", "ws-ext", "External WS")
if !handled {
t.Error("expected handled=true when URL present")
}
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
if resp["url"] != "http://external.example/a2a" {
t.Errorf("got url %v", resp["url"])
}
if resp["name"] != "External WS" {
t.Errorf("got name %v", resp["name"])
}
}
func TestWriteExternalWorkspaceURL_NoURL_FallsThrough(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
mock.ExpectQuery(`SELECT COALESCE\(url,''\) FROM workspaces WHERE id =`).
WithArgs("ws-ext").
WillReturnRows(sqlmock.NewRows([]string{"url"}).AddRow(""))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/x", nil)
if handled := writeExternalWorkspaceURL(context.Background(), c, "ws-caller", "ws-ext", ""); handled {
t.Error("expected handled=false when URL empty")
}
}
func TestWriteExternalWorkspaceURL_RewritesLocalhostForDockerCaller(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
mock.ExpectQuery(`SELECT COALESCE\(url,''\) FROM workspaces WHERE id =`).
WithArgs("ws-ext").
WillReturnRows(sqlmock.NewRows([]string{"url"}).AddRow("http://127.0.0.1:8000/a2a"))
// non-external caller runtime → rewrite enabled
mock.ExpectQuery(`SELECT COALESCE\(runtime,'langgraph'\) FROM workspaces WHERE id =`).
WithArgs("ws-caller").
WillReturnRows(sqlmock.NewRows([]string{"runtime"}).AddRow("langgraph"))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/x", nil)
writeExternalWorkspaceURL(context.Background(), c, "ws-caller", "ws-ext", "")
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
if resp["url"] != "http://host.docker.internal:8000/a2a" {
t.Errorf("expected 127.0.0.1 → host.docker.internal rewrite, got %v", resp["url"])
}
}
// --- discoverHostPeer smoke (currently unreachable via Discover) ---
func TestDiscoverHostPeer_Smoke_CacheHit(t *testing.T) {
setupTestDB(t)
mr := setupTestRedis(t)
mr.Set("ws:ws-host:url", "http://hostcache.example")
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/x", nil)
discoverHostPeer(context.Background(), c, "ws-host")
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestDiscoverHostPeer_Smoke_NotFound(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
mock.ExpectQuery(`SELECT url, status, forwarded_to FROM workspaces WHERE id =`).
WithArgs("ws-none").
WillReturnError(sql.ErrNoRows)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/x", nil)
discoverHostPeer(context.Background(), c, "ws-none")
if w.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d", w.Code)
}
}
func TestDiscoverHostPeer_Smoke_DBError(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
mock.ExpectQuery(`SELECT url, status, forwarded_to FROM workspaces WHERE id =`).
WithArgs("ws-err").
WillReturnError(sql.ErrConnDone)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/x", nil)
discoverHostPeer(context.Background(), c, "ws-err")
if w.Code != http.StatusInternalServerError {
t.Errorf("expected 500, got %d", w.Code)
}
}
func TestDiscoverHostPeer_Smoke_ForwardedChainAndNullURL(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
mock.ExpectQuery(`SELECT url, status, forwarded_to FROM workspaces WHERE id =`).
WithArgs("ws-a").
WillReturnRows(sqlmock.NewRows([]string{"url", "status", "forwarded_to"}).AddRow(nil, "online", "ws-b"))
mock.ExpectQuery(`SELECT url, status, forwarded_to FROM workspaces WHERE id =`).
WithArgs("ws-b").
WillReturnRows(sqlmock.NewRows([]string{"url", "status", "forwarded_to"}).AddRow(nil, "offline", nil))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/x", nil)
discoverHostPeer(context.Background(), c, "ws-a")
if w.Code != http.StatusServiceUnavailable {
t.Errorf("expected 503 for null URL after chain, got %d", w.Code)
}
}
func TestDiscoverHostPeer_Smoke_Success(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
mock.ExpectQuery(`SELECT url, status, forwarded_to FROM workspaces WHERE id =`).
WithArgs("ws-ok").
WillReturnRows(sqlmock.NewRows([]string{"url", "status", "forwarded_to"}).AddRow("http://ok.example", "online", nil))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/x", nil)
discoverHostPeer(context.Background(), c, "ws-ok")
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}