package api import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "visiona-backend/internal/session" ) // fakeSessionStore 是測試用 Store 實作,只回 List 結果;其他方法 panic 表示 // 不應被呼叫(以利早期偵錯)。 type fakeSessionStore struct { sessions []*session.Summary listErr error } func (f *fakeSessionStore) Register(context.Context, string, session.Handle) error { panic("Register should not be called") } func (f *fakeSessionStore) Unregister(context.Context, string) error { panic("Unregister should not be called") } func (f *fakeSessionStore) Lookup(context.Context, string) (session.Handle, error) { panic("Lookup should not be called") } func (f *fakeSessionStore) Exists(context.Context, string) (bool, error) { panic("Exists should not be called") } func (f *fakeSessionStore) List(context.Context) ([]*session.Summary, error) { return f.sessions, f.listErr } func (f *fakeSessionStore) Heartbeat(context.Context, string) error { panic("Heartbeat should not be called") } func (f *fakeSessionStore) CleanupExpired(context.Context, time.Duration) (int, error) { panic("CleanupExpired should not be called") } // TestHealthzHandler 驗證 /healthz 回 200 + status:ok。 func TestHealthzHandler(t *testing.T) { r := gin.New() r.GET("/healthz", HealthzHandler()) w := httptest.NewRecorder() r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/healthz", nil)) assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), `"status":"ok"`) } // TestSystemHealth_TunnelDisconnected 驗證沒 session 時回 connected=false。 func TestSystemHealth_TunnelDisconnected(t *testing.T) { r := gin.New() r.Use(RequestIDMiddleware()) g := r.Group("/api") registerSystemRoutes(g, Deps{ SessionStore: &fakeSessionStore{sessions: nil}, Logger: nil, }) w := httptest.NewRecorder() r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/api/system/health", nil)) require.Equal(t, http.StatusOK, w.Code) var body SuccessBody require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) require.True(t, body.Success) data, _ := body.Data.(map[string]any) require.NotNil(t, data) assert.Equal(t, "ok", data["api_server"]) assert.Equal(t, false, data["tunnel_connected"]) } // TestSystemHealth_TunnelConnected 驗證有 session 時回 connected=true 並帶 last_seen_at。 func TestSystemHealth_TunnelConnected(t *testing.T) { now := time.Now().UTC().Truncate(time.Second) r := gin.New() r.Use(RequestIDMiddleware()) g := r.Group("/api") registerSystemRoutes(g, Deps{ SessionStore: &fakeSessionStore{ sessions: []*session.Summary{ {Token: "vAc_a", LastHeartbeat: now.Add(-5 * time.Second)}, {Token: "vAc_b", LastHeartbeat: now}, // 最新 }, }, }) w := httptest.NewRecorder() r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/api/system/health", nil)) require.Equal(t, http.StatusOK, w.Code) var body SuccessBody require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) data := body.Data.(map[string]any) assert.Equal(t, true, data["tunnel_connected"]) assert.EqualValues(t, 2, data["agent_session_count"]) assert.NotEmpty(t, data["agent_last_seen_at"]) } // TestSystemInfo 驗證 GET /api/system/info 回基本欄位。 func TestSystemInfo(t *testing.T) { r := gin.New() r.Use(RequestIDMiddleware()) g := r.Group("/api") registerSystemRoutes(g, Deps{}) w := httptest.NewRecorder() r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/api/system/info", nil)) require.Equal(t, http.StatusOK, w.Code) var body SuccessBody require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) data := body.Data.(map[string]any) assert.Equal(t, "visiona-api-server", data["service"]) assert.Equal(t, "phase-0-prototype", data["phase"]) }