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/device" ) // newDevicesFixture 建立 router 並塞好必要依賴(InMemory repo + fakeSessionStore)。 // // Phase 0.7 security fix C1:移除 Deps.StaticUserID(見 .autoflow/05-implementation/review/phase-0.7-security-audit.md)。 // 改由 injectStaticUserContext 顯式注入 UserContext,handler 強制要求 UserContext 非空。 func newDevicesFixture(t *testing.T, sessions []any) *gin.Engine { t.Helper() r := gin.New() r.Use(RequestIDMiddleware()) r.Use(injectStaticUserContext("demo-user", "")) g := r.Group("/api") _ = sessions // 暫用,下方 helper 內建 registerDeviceRoutes(g, Deps{ DeviceRepo: device.NewInMemoryRepository(), SessionStore: &fakeSessionStore{}, // 無 session }) return r } // TestDevicesList_Empty 驗證沒 device 時回空陣列。 func TestDevicesList_Empty(t *testing.T) { r := newDevicesFixture(t, nil) w := httptest.NewRecorder() r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/api/devices", nil)) require.Equal(t, http.StatusOK, w.Code) var sb SuccessBody require.NoError(t, json.Unmarshal(w.Body.Bytes(), &sb)) arr, ok := sb.Data.([]any) require.True(t, ok) assert.Empty(t, arr) } // TestDevicesList_ReturnsOwnDevicesOnly 驗證只回當前 user 的 device。 func TestDevicesList_ReturnsOwnDevicesOnly(t *testing.T) { repo := device.NewInMemoryRepository() ctx := context.Background() now := time.Now().UTC() require.NoError(t, repo.Save(ctx, &device.Device{ ID: "mine", OwnerUserID: "demo-user", Name: "A", DeviceType: "kl520", RemoteStatus: device.RemoteStatusOnline, Status: device.USBStatusOnline, CreatedAt: now, })) require.NoError(t, repo.Save(ctx, &device.Device{ ID: "theirs", OwnerUserID: "other", Name: "B", DeviceType: "kl520", CreatedAt: now, })) r := gin.New() r.Use(RequestIDMiddleware()) r.Use(injectStaticUserContext("demo-user", "")) g := r.Group("/api") registerDeviceRoutes(g, Deps{ DeviceRepo: repo, SessionStore: &fakeSessionStore{}, }) w := httptest.NewRecorder() r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/api/devices", nil)) require.Equal(t, http.StatusOK, w.Code) var sb SuccessBody require.NoError(t, json.Unmarshal(w.Body.Bytes(), &sb)) arr := sb.Data.([]any) require.Len(t, arr, 1, "只應看到自己的 device") first := arr[0].(map[string]any) assert.Equal(t, "mine", first["id"]) assert.Equal(t, false, first["tunnel_online"], "沒 session → tunnel_online=false") } // TestDevicesGet_NotOwner 驗證非 owner 被擋 403。 func TestDevicesGet_NotOwner(t *testing.T) { repo := device.NewInMemoryRepository() require.NoError(t, repo.Save(context.Background(), &device.Device{ ID: "x", OwnerUserID: "other", Name: "a", DeviceType: "kl520", })) r := gin.New() r.Use(RequestIDMiddleware()) r.Use(injectStaticUserContext("demo-user", "")) g := r.Group("/api") registerDeviceRoutes(g, Deps{ DeviceRepo: repo, SessionStore: &fakeSessionStore{}, }) w := httptest.NewRecorder() r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/api/devices/x", nil)) assert.Equal(t, http.StatusForbidden, w.Code) } // TestDevicesGet_NotFound 驗證不存在回 404。 func TestDevicesGet_NotFound(t *testing.T) { r := newDevicesFixture(t, nil) w := httptest.NewRecorder() r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/api/devices/ghost", nil)) assert.Equal(t, http.StatusNotFound, w.Code) }