server/internal/handlers/admin_users_test.go
Rama main · réplica de lectura del repositorio original.
package handlers_test
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"blackbox/server/internal/auth"
"blackbox/server/internal/handlers"
"blackbox/server/internal/hub"
"blackbox/server/internal/models"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func adminUserContext(userID string) context.Context {
return context.WithValue(context.Background(), auth.ClaimsKey, &auth.Claims{
UserID: userID,
IsAdmin: true,
})
}
func TestListAdminUsers_ReturnsList(t *testing.T) {
database := newTestDB(t)
database.Create(&models.User{ID: "u1", Username: "alice", IsAdmin: true})
database.Create(&models.User{ID: "u2", Username: "bob", IsAdmin: false})
req := httptest.NewRequest(http.MethodGet, "/api/admin/users", nil)
req = req.WithContext(adminUserContext("u1"))
w := httptest.NewRecorder()
handlers.ListAdminUsers(database)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var result []map[string]interface{}
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
assert.Len(t, result, 2)
// password hash must not be in response
for _, u := range result {
_, hasHash := u["password_hash"]
assert.False(t, hasHash)
}
}
func TestUpdateAdminUser_ToggleAdmin(t *testing.T) {
database := newTestDB(t)
database.Create(&models.User{ID: "admin1", Username: "admin", IsAdmin: true})
database.Create(&models.User{ID: "user1", Username: "alice", IsAdmin: false})
body := `{"is_admin": true}`
req := httptest.NewRequest(http.MethodPatch, "/api/admin/users/user1", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(adminUserContext("admin1"))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "user1")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
w := httptest.NewRecorder()
handlers.UpdateAdminUser(database, nil)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var user models.User
require.NoError(t, database.First(&user, "id = ?", "user1").Error)
assert.True(t, user.IsAdmin)
assert.Equal(t, 1, user.TokenVersion)
}
func TestUpdateAdminUser_CannotDemoteSelf(t *testing.T) {
database := newTestDB(t)
database.Create(&models.User{ID: "admin1", Username: "admin", IsAdmin: true})
body := `{"is_admin": false}`
req := httptest.NewRequest(http.MethodPatch, "/api/admin/users/admin1", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(adminUserContext("admin1"))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "admin1")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
w := httptest.NewRecorder()
handlers.UpdateAdminUser(database, nil)(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
}
func TestUpdateAdminUser_RequiresExplicitIsAdmin(t *testing.T) {
database := newTestDB(t)
require.NoError(t, database.Create(&models.User{ID: "admin1", Username: "admin", IsAdmin: true}).Error)
require.NoError(t, database.Create(&models.User{ID: "user1", Username: "alice", IsAdmin: false}).Error)
req := httptest.NewRequest(http.MethodPatch, "/api/admin/users/user1", bytes.NewBufferString(`{}`))
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(adminUserContext("admin1"))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "user1")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
w := httptest.NewRecorder()
handlers.UpdateAdminUser(database, nil)(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestUpdateAdminUser_InvalidatesActiveSessionsOnRoleChange(t *testing.T) {
database := newTestDB(t)
require.NoError(t, database.Create(&models.User{ID: "admin1", Username: "admin", IsAdmin: true}).Error)
require.NoError(t, database.Create(&models.User{ID: "user1", Username: "alice", IsAdmin: false, TokenVersion: 7}).Error)
eventHub := hub.New()
_, _, disconnect, unsub, err := eventHub.Subscribe("user1", "10.0.0.1")
require.NoError(t, err)
defer unsub()
req := httptest.NewRequest(http.MethodPatch, "/api/admin/users/user1", bytes.NewBufferString(`{"is_admin": true}`))
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(adminUserContext("admin1"))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "user1")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
w := httptest.NewRecorder()
handlers.UpdateAdminUser(database, eventHub)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
select {
case reason := <-disconnect:
assert.Equal(t, "session invalidated", reason)
case <-time.After(100 * time.Millisecond):
t.Fatal("role change did not invalidate active sessions")
}
}
func TestForceLogoutUser_IncrementsTokenVersion(t *testing.T) {
database := newTestDB(t)
database.Create(&models.User{ID: "admin1", Username: "admin", IsAdmin: true, TokenVersion: 0})
database.Create(&models.User{ID: "user1", Username: "alice", IsAdmin: false, TokenVersion: 0})
req := httptest.NewRequest(http.MethodPost, "/api/admin/users/user1/force-logout", nil)
req = req.WithContext(adminUserContext("admin1"))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "user1")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
w := httptest.NewRecorder()
handlers.ForceLogoutUser(database, nil)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var user models.User
require.NoError(t, database.First(&user, "id = ?", "user1").Error)
assert.Equal(t, 1, user.TokenVersion)
}
func TestDeleteAdminUser_DeletesUser(t *testing.T) {
database := newTestDB(t)
database.Create(&models.User{ID: "admin1", Username: "admin", IsAdmin: true})
database.Create(&models.User{ID: "user1", Username: "alice", IsAdmin: false})
req := httptest.NewRequest(http.MethodDelete, "/api/admin/users/user1", nil)
req = req.WithContext(adminUserContext("admin1"))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "user1")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
w := httptest.NewRecorder()
handlers.DeleteAdminUser(database, nil)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var count int64
database.Model(&models.User{}).Where("id = ?", "user1").Count(&count)
assert.EqualValues(t, 0, count)
}
func TestDeleteAdminUser_CannotDeleteSelf(t *testing.T) {
database := newTestDB(t)
database.Create(&models.User{ID: "admin1", Username: "admin", IsAdmin: true})
req := httptest.NewRequest(http.MethodDelete, "/api/admin/users/admin1", nil)
req = req.WithContext(adminUserContext("admin1"))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "admin1")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
w := httptest.NewRecorder()
handlers.DeleteAdminUser(database, nil)(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
}