4bce095964
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>
270 lines
9.2 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|