jim800121chen adab000987 feat(camera): camera/media 推論改走 tunnel proxy(塊1,取代 501 stub)
推論工作區後端塊1:把 camera/media 9 條路徑從 501 stub 換成走
newProxyHandler 的 tunnel proxy 宣告(比照 devices.go 慣例,新增
camera.go 而非塞 stubs.go)。

- GET /camera/stream → streaming:true(MJPEG multipart 長連線逐 chunk flush)
- 其餘 8 條(camera list/start/stop、media upload×3、batch-images/:index、
  seek)→ request-response(upload 大檔的 request body 由 proxy 直接
  streaming 送出,streaming flag 只控 response)
- 掛在已套 AuthMiddleware 的 apiGroup 下,沿用剝 Origin 修正

WS inference:<deviceId> 結果推播為塊2(ForwardWebSocket);MJPEG <img>
的 stream 認證分支(R-C4)待 security 定案短期 stream-token 方案,本塊未做。

Reviewer 0C/0M/1Mi/3Sug 通過。+camera_test.go 路由黑箱測試(路徑不再
501、無 session 回 502 TUNNEL_DISCONNECTED),全套 api 回歸綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 04:36:06 +08:00

86 lines
3.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"visiona-backend/internal/session"
)
// newCameraFixture 建立一個掛了 registerCameraRoutes 的 router。
//
// 用 fakeSessionStoreList 回空)+ 真實但不 dial 的 Forwarder這樣每條 proxy 路徑
// 在 pickActiveSessionToken 會回 ErrSessionNotFound → 502 TUNNEL_DISCONNECTED。
// 這足以證明「路徑有被註冊、且走的是 proxy handler不是 501 stub」。
//
// injectStaticUserContext 模擬 AuthMiddleware 已放行handler C1 strict mode 要求
// UserContext 非空)。
func newCameraFixture() *gin.Engine {
r := gin.New()
r.Use(RequestIDMiddleware())
r.Use(injectStaticUserContext("demo-user", ""))
g := r.Group("/api")
registerCameraRoutes(g, Deps{
SessionStore: &fakeSessionStore{}, // List 回空 → 無 active session
Forwarder: session.NewForwarder("http://localhost:0", nil),
})
return r
}
// TestCameraRoutes_RegisteredAsProxy 逐一驗證每條 camera/media 路徑:
// - 不再回 501代表不是 stub、真的掛了 proxy handler
// - 因無 active session → 回 502 TUNNEL_DISCONNECTEDproxy handler 的預期行為)
//
// 這是「路由宣告正確」的黑箱證據request 有進到 proxy handler、走到 session lookup。
func TestCameraRoutes_RegisteredAsProxy(t *testing.T) {
r := newCameraFixture()
cases := []struct {
method string
path string
}{
{http.MethodGet, "/api/camera/list"},
{http.MethodPost, "/api/camera/start"},
{http.MethodPost, "/api/camera/stop"},
{http.MethodGet, "/api/camera/stream"},
{http.MethodPost, "/api/media/upload/image"},
{http.MethodPost, "/api/media/upload/video"},
{http.MethodPost, "/api/media/upload/batch-images"},
{http.MethodGet, "/api/media/batch-images/0"},
{http.MethodPost, "/api/media/seek"},
}
for _, tc := range cases {
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(tc.method, tc.path, nil))
assert.NotEqual(t, http.StatusNotImplemented, w.Code,
"路徑應走 proxy handler、不再是 501 stub")
assert.Equal(t, http.StatusBadGateway, w.Code,
"無 active session 時 proxy 應回 502 TUNNEL_DISCONNECTED")
assert.Contains(t, w.Body.String(), ErrCodeTunnelDisconnect,
"錯誤碼應為 TUNNEL_DISCONNECTED")
})
}
}
// TestCameraRoutes_NoForwarder 驗證沒注入 Forwarder 時每條路徑回 501
// proxy handler 的依賴缺失分支),確認掛的的確是 newProxyHandler。
func TestCameraRoutes_NoForwarder(t *testing.T) {
r := gin.New()
r.Use(RequestIDMiddleware())
r.Use(injectStaticUserContext("demo-user", ""))
g := r.Group("/api")
registerCameraRoutes(g, Deps{}) // 無 Forwarder / SessionStore
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/api/camera/stream", nil))
assert.Equal(t, http.StatusNotImplemented, w.Code,
"缺 Forwarder 時 proxy handler 回 501證明掛的是 newProxyHandler 而非別的)")
}