Renames: - platform/ → workspace-server/ (Go module path stays as "platform" for external dep compat — will update after plugin module republish) - workspace-template/ → workspace/ Removed (moved to separate repos or deleted): - PLAN.md — internal roadmap (move to private project board) - HANDOFF.md, AGENTS.md — one-time internal session docs - .claude/ — gitignored entirely (local agent config) - infra/cloudflare-worker/ → Molecule-AI/molecule-tenant-proxy - org-templates/molecule-dev/ → standalone template repo - .mcp-eval/ → molecule-mcp-server repo - test-results/ — ephemeral, gitignored Security scrubbing: - Cloudflare account/zone/KV IDs → placeholders - Real EC2 IPs → <EC2_IP> in all docs - CF token prefix, Neon project ID, Fly app names → redacted - Langfuse dev credentials → parameterized - Personal runner username/machine name → generic Community files: - CONTRIBUTING.md — build, test, branch conventions - CODE_OF_CONDUCT.md — Contributor Covenant 2.1 All Dockerfiles, CI workflows, docker-compose, railway.toml, render.yaml, README, CLAUDE.md updated for new directory names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
159 lines
4.2 KiB
Go
159 lines
4.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// ==================== GET /events (List) ====================
|
|
|
|
func TestEventsList_Success(t *testing.T) {
|
|
mock := setupTestDB(t)
|
|
setupTestRedis(t)
|
|
handler := NewEventsHandler()
|
|
|
|
now := time.Now()
|
|
wsID := "ws-evt-1"
|
|
rows := sqlmock.NewRows([]string{"id", "event_type", "workspace_id", "payload", "created_at"}).
|
|
AddRow("evt-1", "WORKSPACE_ONLINE", &wsID, []byte(`{"name":"Agent A"}`), now).
|
|
AddRow("evt-2", "WORKSPACE_OFFLINE", &wsID, []byte(`{"name":"Agent A"}`), now)
|
|
|
|
mock.ExpectQuery("SELECT id, event_type, workspace_id, payload, created_at").
|
|
WillReturnRows(rows)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest("GET", "/events", nil)
|
|
|
|
handler.List(c)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected 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 len(resp) != 2 {
|
|
t.Errorf("expected 2 events, got %d", len(resp))
|
|
}
|
|
if resp[0]["event_type"] != "WORKSPACE_ONLINE" {
|
|
t.Errorf("expected event_type 'WORKSPACE_ONLINE', got %v", resp[0]["event_type"])
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("unmet sqlmock expectations: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEventsList_Empty(t *testing.T) {
|
|
mock := setupTestDB(t)
|
|
setupTestRedis(t)
|
|
handler := NewEventsHandler()
|
|
|
|
mock.ExpectQuery("SELECT id, event_type, workspace_id, payload, created_at").
|
|
WillReturnRows(sqlmock.NewRows([]string{"id", "event_type", "workspace_id", "payload", "created_at"}))
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest("GET", "/events", nil)
|
|
|
|
handler.List(c)
|
|
|
|
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 len(resp) != 0 {
|
|
t.Errorf("expected empty list, got %d events", len(resp))
|
|
}
|
|
}
|
|
|
|
func TestEventsList_DBError(t *testing.T) {
|
|
mock := setupTestDB(t)
|
|
setupTestRedis(t)
|
|
handler := NewEventsHandler()
|
|
|
|
mock.ExpectQuery("SELECT id, event_type, workspace_id, payload, created_at").
|
|
WillReturnError(sql.ErrConnDone)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest("GET", "/events", nil)
|
|
|
|
handler.List(c)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Errorf("expected 500, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// ==================== GET /events/:workspaceId (ListByWorkspace) ====================
|
|
|
|
func TestEventsListByWorkspace_Success(t *testing.T) {
|
|
mock := setupTestDB(t)
|
|
setupTestRedis(t)
|
|
handler := NewEventsHandler()
|
|
|
|
now := time.Now()
|
|
wsID := "ws-by-ws"
|
|
rows := sqlmock.NewRows([]string{"id", "event_type", "workspace_id", "payload", "created_at"}).
|
|
AddRow("evt-3", "WORKSPACE_PROVISIONING", &wsID, []byte(`{"name":"Agent B"}`), now)
|
|
|
|
mock.ExpectQuery("SELECT id, event_type, workspace_id, payload, created_at").
|
|
WithArgs("ws-by-ws").
|
|
WillReturnRows(rows)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Params = gin.Params{{Key: "workspaceId", Value: "ws-by-ws"}}
|
|
c.Request = httptest.NewRequest("GET", "/events/ws-by-ws", nil)
|
|
|
|
handler.ListByWorkspace(c)
|
|
|
|
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 len(resp) != 1 {
|
|
t.Errorf("expected 1 event, got %d", len(resp))
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("unmet sqlmock expectations: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEventsListByWorkspace_DBError(t *testing.T) {
|
|
mock := setupTestDB(t)
|
|
setupTestRedis(t)
|
|
handler := NewEventsHandler()
|
|
|
|
mock.ExpectQuery("SELECT id, event_type, workspace_id, payload, created_at").
|
|
WithArgs("ws-err").
|
|
WillReturnError(sql.ErrConnDone)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Params = gin.Params{{Key: "workspaceId", Value: "ws-err"}}
|
|
c.Request = httptest.NewRequest("GET", "/events/ws-err", nil)
|
|
|
|
handler.ListByWorkspace(c)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Errorf("expected 500, got %d", w.Code)
|
|
}
|
|
}
|