fix(api): /users/@me отдаёт custom_status_emoji
Редактор профиля в настройках читает `user.custom_status_emoji` и вызывает
`.trim()` прямо при отрисовке, а ручка это поле не возвращала: открытие
«Настройки → Профиль» падало с «Cannot read properties of undefined (reading
'trim')» (дефект с 22.09, commit 085454d — клиент поле требовал, сервер не
отдавал). Теперь поле приходит всегда, как и `custom_status`.
Найдено живой проверкой на стенде; тест `profile_payload_test.go` проверяет, что
ключ есть в ответе даже с пустым значением, и что PATCH его сохраняет.
This commit is contained in:
@@ -15,17 +15,20 @@ import (
|
|||||||
|
|
||||||
// profilePayload — публичный профиль пользователя (AGENT.md 8.2).
|
// profilePayload — публичный профиль пользователя (AGENT.md 8.2).
|
||||||
type profilePayload struct {
|
type profilePayload struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
Bio string `json:"bio"`
|
Bio string `json:"bio"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CustomStatus string `json:"custom_status"`
|
CustomStatus string `json:"custom_status"`
|
||||||
AvatarFileID string `json:"avatar_file_id,omitempty"`
|
// CustomStatusEmoji — эмодзи статуса: клиент подставляет его в редактор
|
||||||
BannerFileID string `json:"banner_file_id,omitempty"`
|
// профиля, поэтому поле отдаётся всегда, а не только непустым.
|
||||||
IsInstanceAdmin bool `json:"is_instance_admin"`
|
CustomStatusEmoji string `json:"custom_status_emoji"`
|
||||||
Badges []string `json:"badges"`
|
AvatarFileID string `json:"avatar_file_id,omitempty"`
|
||||||
Locale string `json:"locale"`
|
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 — признак пройденной первичной настройки (AGENT.md 7.2).
|
||||||
OnboardingCompleted bool `json:"onboarding_completed"`
|
OnboardingCompleted bool `json:"onboarding_completed"`
|
||||||
// TOTPEnabled — включена ли 2FA: клиенту нужно знать, требовать ли код
|
// TOTPEnabled — включена ли 2FA: клиенту нужно знать, требовать ли код
|
||||||
@@ -63,6 +66,7 @@ func profileFromUser(user *store.User, includePrivate bool) profilePayload {
|
|||||||
Bio: user.Bio,
|
Bio: user.Bio,
|
||||||
Status: user.Status,
|
Status: user.Status,
|
||||||
CustomStatus: user.CustomStatus,
|
CustomStatus: user.CustomStatus,
|
||||||
|
CustomStatusEmoji: user.CustomStatusEmoji,
|
||||||
IsInstanceAdmin: user.IsInstanceAdmin,
|
IsInstanceAdmin: user.IsInstanceAdmin,
|
||||||
Badges: user.Badges,
|
Badges: user.Badges,
|
||||||
OnboardingCompleted: user.OnboardingCompletedAt != nil,
|
OnboardingCompleted: user.OnboardingCompletedAt != nil,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user