feat(infra): auto-commit con 86 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user