package identity import ( "bytes" "io" "log/slog" "net/http" "net/http/httptest" "testing" "time" "yovision.local/sense/internal/platform" ) func TestHTTPLoginCookieAndProductBoundary(t *testing.T) { store := NewMemoryStore() cfg := Config{SigningKey: []byte("0123456789abcdef0123456789abcdef"), BootstrapToken: "bootstrap-test", SessionTTL: time.Hour} service, err := NewService(store, cfg) if err != nil { t.Fatal(err) } app := platform.NewApp(platform.Config{DatabaseMode: platform.DatabaseModeMemory}, nil, slog.New(slog.NewTextHandler(io.Discard, nil))) NewModule(service, cfg).Register(app) app.Handle("GET /same-origin-frame", RequireSameOriginFrame(PermissionMediaRead, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) }))) bootstrap := httptest.NewRequest(http.MethodPost, "/api/v1/identity/bootstrap", bytes.NewBufferString(`{"username":"admin","display_name":"管理员","password":"StrongPass2026"}`)) bootstrap.Header.Set("X-Sense-Bootstrap-Token", "bootstrap-test") bootstrapResult := httptest.NewRecorder() app.Handler().ServeHTTP(bootstrapResult, bootstrap) if bootstrapResult.Code != http.StatusCreated { t.Fatalf("bootstrap status = %d, body = %s", bootstrapResult.Code, bootstrapResult.Body.String()) } login := httptest.NewRequest(http.MethodPost, "/api/v1/identity/login", bytes.NewBufferString(`{"username":"admin","password":"StrongPass2026"}`)) loginResult := httptest.NewRecorder() app.Handler().ServeHTTP(loginResult, login) if loginResult.Code != http.StatusOK { t.Fatalf("login status = %d, body = %s", loginResult.Code, loginResult.Body.String()) } cookies := loginResult.Result().Cookies() if len(cookies) != 1 || cookies[0].Name != sessionCookieName || !cookies[0].HttpOnly || cookies[0].SameSite != http.SameSiteStrictMode { t.Fatalf("unexpected session cookies: %#v", cookies) } me := httptest.NewRequest(http.MethodGet, "/api/v1/identity/me", nil) me.AddCookie(cookies[0]) me.Header.Set("X-Product", "bell") foreignResult := httptest.NewRecorder() app.Handler().ServeHTTP(foreignResult, me) if foreignResult.Code != http.StatusUnauthorized { t.Fatalf("Bell product header status = %d", foreignResult.Code) } me.Header.Set("X-Product", "sense") senseResult := httptest.NewRecorder() app.Handler().ServeHTTP(senseResult, me) if senseResult.Code != http.StatusOK { t.Fatalf("Sense product header status = %d", senseResult.Code) } meWithoutProduct := httptest.NewRequest(http.MethodGet, "/api/v1/identity/me", nil) meWithoutProduct.AddCookie(cookies[0]) missingProductResult := httptest.NewRecorder() app.Handler().ServeHTTP(missingProductResult, meWithoutProduct) if missingProductResult.Code != http.StatusUnauthorized { t.Fatalf("missing product header status = %d", missingProductResult.Code) } frame := httptest.NewRequest(http.MethodGet, "/same-origin-frame", nil) frame.AddCookie(cookies[0]) frame.Header.Set("Sec-Fetch-Site", "same-origin") frame.Header.Set("Sec-Fetch-Mode", "navigate") frame.Header.Set("Sec-Fetch-Dest", "iframe") frameResult := httptest.NewRecorder() app.Handler().ServeHTTP(frameResult, frame) if frameResult.Code != http.StatusNoContent { t.Fatalf("same-origin frame status = %d, body = %s", frameResult.Code, frameResult.Body.String()) } frame.Header.Set("Sec-Fetch-Site", "cross-site") crossSiteResult := httptest.NewRecorder() app.Handler().ServeHTTP(crossSiteResult, frame) if crossSiteResult.Code != http.StatusUnauthorized { t.Fatalf("cross-site frame status = %d", crossSiteResult.Code) } missingCookie := httptest.NewRequest(http.MethodGet, "/same-origin-frame", nil) missingCookie.Header.Set("Sec-Fetch-Site", "same-origin") missingCookie.Header.Set("Sec-Fetch-Mode", "navigate") missingCookie.Header.Set("Sec-Fetch-Dest", "iframe") missingCookieResult := httptest.NewRecorder() app.Handler().ServeHTTP(missingCookieResult, missingCookie) if missingCookieResult.Code != http.StatusUnauthorized { t.Fatalf("missing-cookie frame status = %d", missingCookieResult.Code) } }