Files
fn_registry/functions/infra/synapse_admin_client_test.go
T
egutierrez 621e8895c9 feat(infra): auto-commit con 86 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:38:15 +02:00

278 lines
9.0 KiB
Go

package infra
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func newSynapseTestServer(t *testing.T) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
// GET /_synapse/admin/v2/users (list)
// Note: exact path match (no trailing slash) catches the list endpoint only.
mux.HandleFunc("/_synapse/admin/v2/users", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
return
}
if r.Header.Get("Authorization") == "Bearer bad" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(`{"errcode":"M_FORBIDDEN","error":"not admin"}`))
return
}
w.Header().Set("Content-Type", "application/json")
nextToken := 2
json.NewEncoder(w).Encode(map[string]interface{}{
"users": []map[string]interface{}{
{"name": "@alice:server", "admin": true, "deactivated": false, "creation_ts": 1000},
{"name": "@bob:server", "admin": false, "deactivated": false, "creation_ts": 2000},
},
"total": 2,
"next_token": nextToken,
})
})
// GET /_synapse/admin/v2/users/{userID} (single user + devices)
mux.HandleFunc("/_synapse/admin/v2/users/", func(w http.ResponseWriter, r *http.Request) {
suffix := strings.TrimPrefix(r.URL.Path, "/_synapse/admin/v2/users/")
// devices sub-path
if strings.HasSuffix(suffix, "/devices") {
if r.Method != http.MethodGet {
http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"devices": []map[string]interface{}{
{"device_id": "AABBCC", "display_name": "Alice's phone", "last_seen_ip": "1.2.3.4", "last_seen_ts": 9999},
{"device_id": "DDEEFF", "display_name": "Alice's laptop", "last_seen_ip": "5.6.7.8", "last_seen_ts": 8888},
},
"total": 2,
})
return
}
// single device delete sub-path: /{userID}/devices/{deviceID}
if strings.Contains(suffix, "/devices/") {
if r.Method != http.MethodDelete {
http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{}`))
return
}
// single user GET
if r.Method != http.MethodGet {
http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
return
}
// 404 for missing user
if strings.Contains(suffix, "missing") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"errcode":"M_NOT_FOUND","error":"User not found"}`))
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(AdminUser{
UserID: "@alice:server",
DisplayName: "Alice",
Admin: true,
})
})
// POST /_synapse/admin/v1/deactivate/{userID}
mux.HandleFunc("/_synapse/admin/v1/deactivate/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
return
}
body, _ := io.ReadAll(r.Body)
var req map[string]interface{}
if err := json.Unmarshal(body, &req); err != nil {
http.Error(w, `{"errcode":"M_BAD_JSON","error":"bad json"}`, http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"id_server_unbind_result": "success"})
})
// GET /_synapse/admin/v1/rooms (list)
mux.HandleFunc("/_synapse/admin/v1/rooms", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"rooms": []map[string]interface{}{
{"room_id": "!abc:server", "name": "general", "joined_members": 5},
{"room_id": "!xyz:server", "name": "off-topic", "joined_members": 3},
},
"total_rooms": 2,
})
})
// GET /_synapse/admin/v1/rooms/{roomID}
mux.HandleFunc("/_synapse/admin/v1/rooms/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(AdminRoom{RoomID: "!abc:server", Name: "general", JoinedMembers: 5})
})
// DELETE /_synapse/admin/v2/rooms/{roomID} (async delete)
mux.HandleFunc("/_synapse/admin/v2/rooms/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"delete_id": "del_001"})
})
return httptest.NewServer(mux)
}
func TestSynapseAdminClient(t *testing.T) {
srv := newSynapseTestServer(t)
defer srv.Close()
cl := NewSynapseAdminClient(srv.URL, "mxat_test_token")
ctx := context.Background()
t.Run("ListUsers parses + counts", func(t *testing.T) {
res, err := cl.ListUsers(ctx, ListUsersFilter{From: 0, Limit: 50})
if err != nil {
t.Fatalf("ListUsers: %v", err)
}
if res.TotalCount != 2 {
t.Errorf("TotalCount: got %d, want 2", res.TotalCount)
}
if len(res.Users) != 2 {
t.Fatalf("len(Users): got %d, want 2", len(res.Users))
}
if res.Users[0].UserID != "@alice:server" {
t.Errorf("Users[0].UserID: got %q, want @alice:server", res.Users[0].UserID)
}
if !res.Users[0].Admin {
t.Error("Users[0].Admin should be true")
}
if res.NextToken == nil {
t.Error("NextToken should be non-nil (test server returns next_token=2)")
} else if *res.NextToken != 2 {
t.Errorf("NextToken: got %d, want 2", *res.NextToken)
}
})
t.Run("GetUser inexistente -> error contiene M_NOT_FOUND", func(t *testing.T) {
_, err := cl.GetUser(ctx, "@missing:server")
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "M_NOT_FOUND") {
t.Errorf("error should contain M_NOT_FOUND, got: %v", err)
}
})
t.Run("DeactivateUser ok", func(t *testing.T) {
// Verify via a targeted server that erase=true reaches the body.
var gotErase bool
deactivateSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var req map[string]interface{}
json.Unmarshal(body, &req)
if v, ok := req["erase"].(bool); ok {
gotErase = v
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"id_server_unbind_result": "success"})
}))
defer deactivateSrv.Close()
clDe := NewSynapseAdminClient(deactivateSrv.URL, "tok")
if err := clDe.DeactivateUser(ctx, "@user:server", true); err != nil {
t.Fatalf("DeactivateUser: %v", err)
}
if !gotErase {
t.Error("erase=true not forwarded in request body")
}
})
t.Run("DeleteRoom devuelve delete_id", func(t *testing.T) {
var gotPurge, gotBlock bool
deleteSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, `{}`, 405)
return
}
body, _ := io.ReadAll(r.Body)
var req map[string]interface{}
json.Unmarshal(body, &req)
if v, ok := req["purge"].(bool); ok {
gotPurge = v
}
if v, ok := req["block"].(bool); ok {
gotBlock = v
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"delete_id": "del_007"})
}))
defer deleteSrv.Close()
clDel := NewSynapseAdminClient(deleteSrv.URL, "tok")
deleteID, err := clDel.DeleteRoom(ctx, "!room:server", "cleanup", true, true)
if err != nil {
t.Fatalf("DeleteRoom: %v", err)
}
if deleteID != "del_007" {
t.Errorf("deleteID: got %q, want del_007", deleteID)
}
if !gotPurge {
t.Error("purge=true not forwarded in request body")
}
if !gotBlock {
t.Error("block=true not forwarded in request body")
}
})
t.Run("ListUserDevices parses array", func(t *testing.T) {
devices, err := cl.ListUserDevices(ctx, "@alice:server")
if err != nil {
t.Fatalf("ListUserDevices: %v", err)
}
if len(devices) != 2 {
t.Fatalf("len(devices): got %d, want 2", len(devices))
}
if devices[0].DeviceID != "AABBCC" {
t.Errorf("devices[0].DeviceID: got %q, want AABBCC", devices[0].DeviceID)
}
if devices[0].LastSeenIP != "1.2.3.4" {
t.Errorf("devices[0].LastSeenIP: got %q, want 1.2.3.4", devices[0].LastSeenIP)
}
})
t.Run("HTTP 403 -> error con errcode M_FORBIDDEN", func(t *testing.T) {
badCl := NewSynapseAdminClient(srv.URL, "bad")
_, err := badCl.ListUsers(ctx, ListUsersFilter{})
if err == nil {
t.Fatal("expected error for 403, got nil")
}
if !strings.Contains(err.Error(), "M_FORBIDDEN") {
t.Errorf("error should contain M_FORBIDDEN, got: %v", err)
}
})
}