92633f935e
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 1m7s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
Record the execution platform (kind vk|telegram|direct + device subtype ios|android|web) on each session, captured at creation and carried gateway->backend as a trusted X-Platform header, so the upcoming store-compliance gate has an unforgeable execution context. - backend.sessions gains nullable platform_kind/platform_subtype columns (migration 00011, CHECK-constrained, jet regenerated); session.Platform captures them at mint, resolve returns them, middleware exposes platform(c). kind is derived from the establish endpoint, never a client field; the account-merge session mint inherits the caller's platform. - gateway derives the platform (VK subtype from the signed vk_platform via vkauth, Telegram/direct best-effort from the client) and injects X-Platform on every authenticated backend call through the request context. - ui submits a best-effort device subtype on the telegram/guest/email login requests (new FBS subtype field); VK is server-derived from the signed params. - an unattributed session is untrusted (view-only); VK/TG self-heal on the next cold-start re-mint, direct/email on re-login. Signal plumbing only, no user-visible change; X-Platform is inert until the gate consumes it.
111 lines
3.1 KiB
Go
111 lines
3.1 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
|
|
"scrabble/backend/internal/session"
|
|
)
|
|
|
|
// TestRequireUserID checks that the middleware accepts a valid X-User-ID,
|
|
// exposes it through the request context, and rejects missing or malformed
|
|
// headers with 401.
|
|
func TestRequireUserID(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
var seen uuid.UUID
|
|
var ok bool
|
|
r := gin.New()
|
|
r.Use(RequireUserID())
|
|
r.GET("/x", func(c *gin.Context) {
|
|
seen, ok = UserIDFromContext(c.Request.Context())
|
|
c.String(http.StatusOK, "ok")
|
|
})
|
|
|
|
t.Run("valid", func(t *testing.T) {
|
|
seen, ok = uuid.Nil, false
|
|
id := uuid.New()
|
|
req := httptest.NewRequest(http.MethodGet, "/x", nil)
|
|
req.Header.Set("X-User-ID", id.String())
|
|
rec := httptest.NewRecorder()
|
|
r.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", rec.Code)
|
|
}
|
|
if !ok || seen != id {
|
|
t.Fatalf("context id = %s (ok=%v), want %s", seen, ok, id)
|
|
}
|
|
})
|
|
|
|
t.Run("missing", func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
r.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/x", nil))
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("status = %d, want 401", rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("malformed", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/x", nil)
|
|
req.Header.Set("X-User-ID", "not-a-uuid")
|
|
rec := httptest.NewRecorder()
|
|
r.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("status = %d, want 401", rec.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestPlatformContext checks that the middleware parses the gateway-injected
|
|
// X-Platform header into a trusted platform reachable via platform(c), treats an
|
|
// absent or blank-kind header as untrusted, and never rejects the request.
|
|
func TestPlatformContext(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
var seen session.Platform
|
|
var ok bool
|
|
r := gin.New()
|
|
r.Use(platformContext())
|
|
r.GET("/x", func(c *gin.Context) {
|
|
seen, ok = platform(c)
|
|
c.String(http.StatusOK, "ok")
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
header string
|
|
setHeader bool
|
|
wantOK bool
|
|
want session.Platform
|
|
}{
|
|
{"vk-ios", "vk/ios", true, true, session.Platform{Kind: session.PlatformKindVK, Subtype: session.SubtypeIOS}},
|
|
{"telegram-no-subtype", "telegram", true, true, session.Platform{Kind: session.PlatformKindTelegram}},
|
|
{"direct-web", "direct/web", true, true, session.Platform{Kind: session.PlatformKindDirect, Subtype: session.SubtypeWeb}},
|
|
{"absent", "", false, false, session.Platform{}},
|
|
{"empty", "", true, false, session.Platform{}},
|
|
{"blank-kind", "/ios", true, false, session.Platform{}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
seen, ok = session.Platform{}, false
|
|
req := httptest.NewRequest(http.MethodGet, "/x", nil)
|
|
if tc.setHeader {
|
|
req.Header.Set("X-Platform", tc.header)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
r.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200 (X-Platform never rejects)", rec.Code)
|
|
}
|
|
if ok != tc.wantOK || seen != tc.want {
|
|
t.Fatalf("platform = %+v (ok=%v), want %+v (ok=%v)", seen, ok, tc.want, tc.wantOK)
|
|
}
|
|
})
|
|
}
|
|
}
|