feat(api): ручки Фаз(ы) 1 — профиль, серверы, роли, админ инстанса
REST-слой Фазы 1 на huma (OpenAPI 3.1 генерируется из кода):
- профиль: GET/PATCH /users/@me, смена пароля со step-up, публичный профиль,
завершение онбординга (новая миграция 00003 с onboarding_completed_at);
- серверы: создание/изменение/удаление, join/leave, список серверов
пользователя, журнал действий;
- комнаты: список с учётом прав, создание/изменение/удаление;
- участники: список с профилями и ролями, никнейм, тайм-аут, исключение;
- роли: CRUD, выдача/снятие с проверкой иерархии и запретом выдачи прав выше
собственных;
- админ инстанса: публичная информация, настройки, серверы, пользователи,
аудит, выдача прав администратора со step-up; обход лимитов фиксируется в
аудите отдельной записью limits.bypass;
- движок прав: участие в сервере стало обязательным условием (IsMember),
не участник не получает прав роли @user; калькулятор прав общий для API и
Gateway, инвалидация кэша после изменений;
- Gateway: браузерный клиент аутентифицируется cookie на рукопожатии, IDENTIFY
без токена использует её; события GUILD/CHANNEL/MEMBER/ROLE рассылаются из
ручек, USER_UPDATE — адресно;
- ошибки huma отдаются в едином конверте {"error":{"code","message"}}.
Тесты: 8 сценариев API (профиль, жизненный цикл сервера и права, лимиты и
обход админом, иерархия ролей, тайм-аут, скрытие комнаты оверрайдом,
членство в движке прав, cookie-идентификация Gateway).
This commit is contained in:
@@ -60,7 +60,7 @@ func newFixture(t *testing.T) *fixture {
|
||||
t.Fatalf("bootstrap: %v", err)
|
||||
}
|
||||
|
||||
service := gateway.New(st, authService, gateway.NewSnapshot(st), logger, []string{"https://gl.test"})
|
||||
service := gateway.New(st, authService, gateway.NewSnapshot(st, nil), logger, []string{"https://gl.test"})
|
||||
server := httptest.NewServer(service.Handler())
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
@@ -242,6 +242,62 @@ func TestIdentifyWithInvalidToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestIdentifyWithSessionCookie проверяет браузерный вход: токен в IDENTIFY
|
||||
// не передаётся, сессия берётся из cookie рукопожатия (AGENT.md 8.1).
|
||||
func TestIdentifyWithSessionCookie(t *testing.T) {
|
||||
f := newFixture(t)
|
||||
_, token, session, err := f.auth.Register(context.Background(), auth.RegisterInput{
|
||||
Username: "cookie_user", Email: "cookie@example.com", Password: "correct-horse-battery",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("register: %v", err)
|
||||
}
|
||||
if session == nil {
|
||||
t.Fatal("register returned no session")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
url := "ws" + strings.TrimPrefix(f.server.URL, "http")
|
||||
conn, _, err := websocket.Dial(ctx, url, &websocket.DialOptions{
|
||||
HTTPHeader: map[string][]string{
|
||||
"Origin": {"https://gl.test"},
|
||||
"Cookie": {gateway.SessionCookieName + "=" + token},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("dial with cookie: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = conn.CloseNow() })
|
||||
|
||||
if hello := readEnvelope(t, conn); hello.Op != gateway.OpHello {
|
||||
t.Fatalf("first frame op = %d, want HELLO", hello.Op)
|
||||
}
|
||||
// IDENTIFY без токена: сервер доверяет cookie.
|
||||
send(t, conn, gateway.OpIdentify, map[string]any{})
|
||||
ready := readEnvelope(t, conn)
|
||||
if ready.T != "READY" {
|
||||
t.Fatalf("frame = %q, want READY", ready.T)
|
||||
}
|
||||
var snapshot gateway.Ready
|
||||
if err := json.Unmarshal(ready.D, &snapshot); err != nil {
|
||||
t.Fatalf("decode READY: %v", err)
|
||||
}
|
||||
if snapshot.User.Username != "cookie_user" {
|
||||
t.Fatalf("READY user = %q, want cookie_user", snapshot.User.Username)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentifyWithoutTokenOrCookie(t *testing.T) {
|
||||
f := newFixture(t)
|
||||
conn, _ := f.dial(t)
|
||||
send(t, conn, gateway.OpIdentify, map[string]any{})
|
||||
response := readEnvelope(t, conn)
|
||||
if response.Op != gateway.OpInvalidSess {
|
||||
t.Fatalf("op = %d, want INVALID_SESSION", response.Op)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeartbeatAck(t *testing.T) {
|
||||
f := newFixture(t)
|
||||
token := registerUser(t, f, "heartbeat_user", "heartbeat@example.com")
|
||||
|
||||
Reference in New Issue
Block a user