推論工作區後端塊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>
86 lines
3.0 KiB
Go
86 lines
3.0 KiB
Go
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。
|
||
//
|
||
// 用 fakeSessionStore(List 回空)+ 真實但不 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_DISCONNECTED(proxy 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 而非別的)")
|
||
}
|