From 09b4f6878fd78dd0a1d4e17ebf06ea15e494682a Mon Sep 17 00:00:00 2001 From: grendervill Date: Sat, 26 Sep 2026 16:16:17 +0300 Subject: [PATCH] =?UTF-8?q?fix(api):=20/users/@me=20=D0=BE=D1=82=D0=B4?= =?UTF-8?q?=D0=B0=D1=91=D1=82=20custom=5Fstatus=5Femoji?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Редактор профиля в настройках читает `user.custom_status_emoji` и вызывает `.trim()` прямо при отрисовке, а ручка это поле не возвращала: открытие «Настройки → Профиль» падало с «Cannot read properties of undefined (reading 'trim')» (дефект с 22.09, commit 085454d — клиент поле требовал, сервер не отдавал). Теперь поле приходит всегда, как и `custom_status`. Найдено живой проверкой на стенде; тест `profile_payload_test.go` проверяет, что ключ есть в ответе даже с пустым значением, и что PATCH его сохраняет. --- internal/server/api_users.go | 26 ++++++----- internal/server/profile_payload_test.go | 59 +++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 internal/server/profile_payload_test.go diff --git a/internal/server/api_users.go b/internal/server/api_users.go index 7f091f4..60afa53 100644 --- a/internal/server/api_users.go +++ b/internal/server/api_users.go @@ -15,17 +15,20 @@ import ( // profilePayload — публичный профиль пользователя (AGENT.md 8.2). type profilePayload struct { - ID string `json:"id"` - Username string `json:"username"` - DisplayName string `json:"display_name"` - Bio string `json:"bio"` - Status string `json:"status"` - CustomStatus string `json:"custom_status"` - AvatarFileID string `json:"avatar_file_id,omitempty"` - BannerFileID string `json:"banner_file_id,omitempty"` - IsInstanceAdmin bool `json:"is_instance_admin"` - Badges []string `json:"badges"` - Locale string `json:"locale"` + ID string `json:"id"` + Username string `json:"username"` + DisplayName string `json:"display_name"` + Bio string `json:"bio"` + Status string `json:"status"` + CustomStatus string `json:"custom_status"` + // CustomStatusEmoji — эмодзи статуса: клиент подставляет его в редактор + // профиля, поэтому поле отдаётся всегда, а не только непустым. + CustomStatusEmoji string `json:"custom_status_emoji"` + AvatarFileID string `json:"avatar_file_id,omitempty"` + BannerFileID string `json:"banner_file_id,omitempty"` + IsInstanceAdmin bool `json:"is_instance_admin"` + Badges []string `json:"badges"` + Locale string `json:"locale"` // OnboardingCompleted — признак пройденной первичной настройки (AGENT.md 7.2). OnboardingCompleted bool `json:"onboarding_completed"` // TOTPEnabled — включена ли 2FA: клиенту нужно знать, требовать ли код @@ -63,6 +66,7 @@ func profileFromUser(user *store.User, includePrivate bool) profilePayload { Bio: user.Bio, Status: user.Status, CustomStatus: user.CustomStatus, + CustomStatusEmoji: user.CustomStatusEmoji, IsInstanceAdmin: user.IsInstanceAdmin, Badges: user.Badges, OnboardingCompleted: user.OnboardingCompletedAt != nil, diff --git a/internal/server/profile_payload_test.go b/internal/server/profile_payload_test.go new file mode 100644 index 0000000..c71d036 --- /dev/null +++ b/internal/server/profile_payload_test.go @@ -0,0 +1,59 @@ +package server + +import ( + "encoding/json" + "net/http" + "testing" +) + +// TestProfilePayloadCarriesCustomStatusEmoji: редактор профиля читает это поле +// у `GET /users/@me` и вызывает `.trim()`, поэтому поле обязано приходить +// всегда — пустая строка, а не отсутствие ключа (иначе открытие настроек +// падает с «Cannot read properties of undefined»). +func TestProfilePayloadCarriesCustomStatusEmoji(t *testing.T) { + srv, _ := newTestServer(t) + cookie := registerAndLogin(t, srv, "profile_emoji", "profile-emoji@example.com") + + rec := doJSON(t, srv, http.MethodGet, "/api/v1/users/@me", "", cookie) + payload := decodeResponse[struct { + User struct { + CustomStatus string `json:"custom_status"` + CustomStatusEmoji string `json:"custom_status_emoji"` + } `json:"user"` + }](t, rec) + if payload.User.CustomStatus != "" || payload.User.CustomStatusEmoji != "" { + t.Fatalf("у нового пользователя статус пустой: %+v", payload.User) + } + if rec.Body.String() == "" || !jsonHasKey(rec.Body.Bytes(), "custom_status_emoji") { + t.Fatalf("в ответе нет custom_status_emoji: %s", rec.Body.String()) + } + + updated := doJSON(t, srv, http.MethodPatch, "/api/v1/users/@me", + `{"custom_status":"Пишу код","custom_status_emoji":"💻"}`, cookie) + if updated.Code != http.StatusOK { + t.Fatalf("PATCH профиля = %d, body = %s", updated.Code, updated.Body.String()) + } + after := decodeResponse[struct { + User struct { + CustomStatus string `json:"custom_status"` + CustomStatusEmoji string `json:"custom_status_emoji"` + } `json:"user"` + }](t, updated) + if after.User.CustomStatus != "Пишу код" || after.User.CustomStatusEmoji != "💻" { + t.Fatalf("статус не сохранился: %+v", after.User) + } +} + +// jsonHasKey проверяет наличие ключа в JSON-ответе (в т.ч. с пустым значением). +func jsonHasKey(body []byte, key string) bool { + var decoded map[string]any + if err := json.Unmarshal(body, &decoded); err != nil { + return false + } + user, ok := decoded["user"].(map[string]any) + if !ok { + return false + } + _, present := user[key] + return present +}