Files
fn_registry/functions/infra/matrix/matrix_message_send_test.go
T
egutierrez e22c33ee6d refactor(infra): split de drivers pesados a subpaquetes + fix TestSSEHandler
Mueve duckdb_open, clickhouse_open, postgres_open, matrix_* y keyring_token_store
del paquete monolitico functions/infra a subpaquetes propios
(functions/infra/{duckdb,clickhouse,postgres,matrix,keyring}). El paquete infra ya
no importa los drivers (go-duckdb, clickhouse-go, pgx, mautrix, go-keyring), por lo
que las apps que solo usan funciones ligeras (process, cron, http, sqlite) dejan de
arrastrarlos. Reduccion de binarios: dag_engine 72->10MB, registry_api 70->8.7MB,
services_api 70->9MB, call_monitor 68->6.6MB, sqlite_api 70->8.9MB.

Los IDs del registry se mantienen estables (domain: infra en frontmatter). Se
preservan los build tags goolm/libolm de matrix_crypto_init.

Tambien corrige TestSSEHandler: el test leia el body con un unico Read() que con
HTTP chunked solo capturaba el primer evento; ahora usa io.ReadAll hasta EOF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 23:48:59 +02:00

270 lines
9.2 KiB
Go

package matrix
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/id"
)
// newMXTestClient construye un *mautrix.Client apuntando al servidor httptest dado.
func newMXTestClient(t *testing.T, serverURL string) *mautrix.Client {
t.Helper()
cli, err := mautrix.NewClient(serverURL, "@testuser:example.com", "mxat_test_token")
if err != nil {
t.Fatalf("newMXTestClient: %v", err)
}
cli.DeviceID = id.DeviceID("TESTDEVICE01")
return cli
}
// mxSendHandler devuelve un http.Handler que:
// - Acepta PUT /…/rooms/{roomID}/send/{eventType}/{txnID}
// - Devuelve {"event_id": "$fakeEvent123:example.com"} con 200
// - Guarda el body JSON decodificado en bodyOut y la path en pathOut para assertions
func mxSendHandler(t *testing.T, bodyOut *map[string]interface{}, pathOut *string) http.Handler {
t.Helper()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if pathOut != nil {
*pathOut = r.URL.Path
}
if r.Method != http.MethodPut {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if bodyOut != nil {
var parsed map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&parsed); err != nil {
t.Errorf("mxSendHandler: json decode: %v", err)
}
*bodyOut = parsed
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"event_id":"$fakeEvent123:example.com"}`))
})
}
func TestMatrixMessageSend(t *testing.T) {
ctx := context.Background()
const roomID = "!testroom:example.com"
const wantEventID = "$fakeEvent123:example.com"
t.Run("SendText body correcto y EventID parseado", func(t *testing.T) {
var body map[string]interface{}
srv := httptest.NewServer(mxSendHandler(t, &body, nil))
defer srv.Close()
cli := newMXTestClient(t, srv.URL)
evID, err := MatrixSendText(ctx, cli, id.RoomID(roomID), "Hola mundo")
if err != nil {
t.Fatalf("MatrixSendText error: %v", err)
}
if string(evID) != wantEventID {
t.Errorf("EventID: got %q, want %q", evID, wantEventID)
}
if got := body["msgtype"]; got != "m.text" {
t.Errorf("body['msgtype']: got %v, want 'm.text'", got)
}
if got := body["body"]; got != "Hola mundo" {
t.Errorf("body['body']: got %v, want 'Hola mundo'", got)
}
})
t.Run("SendMarkdown bold convierte a HTML strong y sanitiza script", func(t *testing.T) {
var body map[string]interface{}
srv := httptest.NewServer(mxSendHandler(t, &body, nil))
defer srv.Close()
cli := newMXTestClient(t, srv.URL)
evID, err := MatrixSendMarkdown(ctx, cli, id.RoomID(roomID), "**bold**")
if err != nil {
t.Fatalf("MatrixSendMarkdown error: %v", err)
}
if string(evID) != wantEventID {
t.Errorf("EventID: got %q, want %q", evID, wantEventID)
}
// Body debe ser el markdown original como fallback
if got := body["body"]; got != "**bold**" {
t.Errorf("body['body'] fallback: got %v, want '**bold**'", got)
}
// formatted_body debe contener <strong>bold</strong>
fmtBody, ok := body["formatted_body"].(string)
if !ok {
t.Fatalf("formatted_body no es string: %v", body["formatted_body"])
}
if !strings.Contains(fmtBody, "<strong>bold</strong>") {
t.Errorf("formatted_body no contiene <strong>bold</strong>, got: %q", fmtBody)
}
// format debe ser org.matrix.custom.html
if got := body["format"]; got != "org.matrix.custom.html" {
t.Errorf("format: got %v, want 'org.matrix.custom.html'", got)
}
// Sub-test: sanitizer elimina <script>
const xssPayload = `texto <script>alert(1)</script> seguro`
var body2 map[string]interface{}
srv2 := httptest.NewServer(mxSendHandler(t, &body2, nil))
defer srv2.Close()
cli2 := newMXTestClient(t, srv2.URL)
_, err = MatrixSendMarkdown(ctx, cli2, id.RoomID(roomID), xssPayload)
if err != nil {
t.Fatalf("MatrixSendMarkdown XSS error: %v", err)
}
fmtBody2, ok := body2["formatted_body"].(string)
if !ok {
t.Fatalf("formatted_body no es string (XSS test): %v", body2["formatted_body"])
}
// El sanitizer debe eliminar el tag <script>...</script> completo.
// goldmark convierte inline HTML a texto plano antes de sanitizar,
// por lo que el texto interior puede quedar como texto plano — eso es correcto.
if strings.Contains(fmtBody2, "<script>") {
t.Errorf("formatted_body contiene <script> — sanitizer no funciono: %q", fmtBody2)
}
if strings.Contains(fmtBody2, "</script>") {
t.Errorf("formatted_body contiene </script> — sanitizer no funciono: %q", fmtBody2)
}
})
t.Run("SendReply m.relates_to m.in_reply_to presente", func(t *testing.T) {
var body map[string]interface{}
srv := httptest.NewServer(mxSendHandler(t, &body, nil))
defer srv.Close()
cli := newMXTestClient(t, srv.URL)
const parentID = "$parentEvent:example.com"
evID, err := MatrixSendReply(ctx, cli, id.RoomID(roomID), id.EventID(parentID), "ack")
if err != nil {
t.Fatalf("MatrixSendReply error: %v", err)
}
if string(evID) != wantEventID {
t.Errorf("EventID: got %q, want %q", evID, wantEventID)
}
if got := body["body"]; got != "ack" {
t.Errorf("body['body']: got %v, want 'ack'", got)
}
relatesTo, ok := body["m.relates_to"].(map[string]interface{})
if !ok {
t.Fatalf("m.relates_to no es object, got: %v", body["m.relates_to"])
}
inReplyTo, ok := relatesTo["m.in_reply_to"].(map[string]interface{})
if !ok {
t.Fatalf("m.in_reply_to no es object, got: %v", relatesTo["m.in_reply_to"])
}
if got := inReplyTo["event_id"]; got != parentID {
t.Errorf("m.in_reply_to.event_id: got %v, want %q", got, parentID)
}
})
t.Run("EditMessage rel_type m.replace y m.new_content", func(t *testing.T) {
var body map[string]interface{}
srv := httptest.NewServer(mxSendHandler(t, &body, nil))
defer srv.Close()
cli := newMXTestClient(t, srv.URL)
const originalID = "$originalEvent:example.com"
evID, err := MatrixEditMessage(ctx, cli, id.RoomID(roomID), id.EventID(originalID), "texto editado")
if err != nil {
t.Fatalf("MatrixEditMessage error: %v", err)
}
if string(evID) != wantEventID {
t.Errorf("EventID: got %q, want %q", evID, wantEventID)
}
// fallback body
if got := body["body"]; got != "* texto editado" {
t.Errorf("body['body'] fallback: got %v, want '* texto editado'", got)
}
newContent, ok := body["m.new_content"].(map[string]interface{})
if !ok {
t.Fatalf("m.new_content no es object, got: %v", body["m.new_content"])
}
if got := newContent["body"]; got != "texto editado" {
t.Errorf("m.new_content.body: got %v, want 'texto editado'", got)
}
relatesTo, ok := body["m.relates_to"].(map[string]interface{})
if !ok {
t.Fatalf("m.relates_to no es object, got: %v", body["m.relates_to"])
}
if got := relatesTo["rel_type"]; got != "m.replace" {
t.Errorf("m.relates_to.rel_type: got %v, want 'm.replace'", got)
}
if got := relatesTo["event_id"]; got != originalID {
t.Errorf("m.relates_to.event_id: got %v, want %q", got, originalID)
}
})
t.Run("SendReaction tipo m.reaction con m.annotation y key", func(t *testing.T) {
var body map[string]interface{}
var capturedPath string
srv := httptest.NewServer(mxSendHandler(t, &body, &capturedPath))
defer srv.Close()
cli := newMXTestClient(t, srv.URL)
const targetID = "$targetEvent:example.com"
evID, err := MatrixSendReaction(ctx, cli, id.RoomID(roomID), id.EventID(targetID), "👍")
if err != nil {
t.Fatalf("MatrixSendReaction error: %v", err)
}
if string(evID) != wantEventID {
t.Errorf("EventID: got %q, want %q", evID, wantEventID)
}
// URL debe contener "m.reaction"
if !strings.Contains(capturedPath, "m.reaction") {
t.Errorf("URL path no contiene 'm.reaction': %q", capturedPath)
}
relatesTo, ok := body["m.relates_to"].(map[string]interface{})
if !ok {
t.Fatalf("m.relates_to no es object, got: %v", body["m.relates_to"])
}
if got := relatesTo["rel_type"]; got != "m.annotation" {
t.Errorf("m.relates_to.rel_type: got %v, want 'm.annotation'", got)
}
if got := relatesTo["key"]; got != "👍" {
t.Errorf("m.relates_to.key: got %v, want '👍'", got)
}
if got := relatesTo["event_id"]; got != targetID {
t.Errorf("m.relates_to.event_id: got %v, want %q", got, targetID)
}
})
t.Run("SendText client nil devuelve error", func(t *testing.T) {
_, err := MatrixSendText(ctx, nil, id.RoomID(roomID), "texto")
if err == nil {
t.Fatal("esperaba error con client nil, got nil")
}
})
t.Run("SendMarkdown client nil devuelve error", func(t *testing.T) {
_, err := MatrixSendMarkdown(ctx, nil, id.RoomID(roomID), "**md**")
if err == nil {
t.Fatal("esperaba error con client nil, got nil")
}
})
t.Run("SendReply client nil devuelve error", func(t *testing.T) {
_, err := MatrixSendReply(ctx, nil, id.RoomID(roomID), "$evID:x", "reply")
if err == nil {
t.Fatal("esperaba error con client nil, got nil")
}
})
t.Run("EditMessage client nil devuelve error", func(t *testing.T) {
_, err := MatrixEditMessage(ctx, nil, id.RoomID(roomID), "$evID:x", "new")
if err == nil {
t.Fatal("esperaba error con client nil, got nil")
}
})
t.Run("SendReaction client nil devuelve error", func(t *testing.T) {
_, err := MatrixSendReaction(ctx, nil, id.RoomID(roomID), "$evID:x", "👍")
if err == nil {
t.Fatal("esperaba error con client nil, got nil")
}
})
}