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 bold fmtBody, ok := body["formatted_body"].(string) if !ok { t.Fatalf("formatted_body no es string: %v", body["formatted_body"]) } if !strings.Contains(fmtBody, "bold") { t.Errorf("formatted_body no contiene bold, 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 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 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, "") { t.Errorf("formatted_body contiene — 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") } }) }