e22c33ee6d
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>
314 lines
8.8 KiB
Go
314 lines
8.8 KiB
Go
//go:build goolm
|
|
|
|
package matrix
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"maunium.net/go/mautrix"
|
|
"maunium.net/go/mautrix/id"
|
|
)
|
|
|
|
// fakeSynapseServer crea un httptest.Server que simula Synapse para tests de sync.
|
|
// syncHandler recibe el numero de llamada /sync (1-indexed) y devuelve la respuesta.
|
|
// nil response significa bloquear hasta ctx cancelado.
|
|
func fakeSynapseServer(t *testing.T, syncFn func(call int, w http.ResponseWriter, r *http.Request)) *httptest.Server {
|
|
t.Helper()
|
|
var callCount int32
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch {
|
|
case r.Method == http.MethodPost && r.URL.Path == "/_matrix/client/v3/user/@alice:localhost/filter":
|
|
// mautrix necesita este endpoint para guardar el filtro; responder con un filter_id
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"filter_id": "f1"})
|
|
case r.URL.Path == "/_matrix/client/v3/sync" || r.URL.Path == "/_matrix/client/r0/sync":
|
|
n := int(atomic.AddInt32(&callCount, 1))
|
|
syncFn(n, w, r)
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
}
|
|
|
|
// syncRespMessage construye una respuesta /sync con un m.room.message.
|
|
func syncRespMessage(nextBatch string) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"next_batch": nextBatch,
|
|
"rooms": map[string]interface{}{
|
|
"join": map[string]interface{}{
|
|
"!testroom:localhost": map[string]interface{}{
|
|
"timeline": map[string]interface{}{
|
|
"events": []interface{}{
|
|
map[string]interface{}{
|
|
"event_id": "$evt001:localhost",
|
|
"type": "m.room.message",
|
|
"sender": "@alice:localhost",
|
|
"origin_server_ts": int64(1700000000000),
|
|
"room_id": "!testroom:localhost",
|
|
"content": map[string]interface{}{
|
|
"msgtype": "m.text",
|
|
"body": "hola mundo",
|
|
},
|
|
},
|
|
},
|
|
"limited": false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// newTestSyncClient crea un *mautrix.Client apuntando al servidor dado.
|
|
func newTestSyncClient(t *testing.T, serverURL string) *mautrix.Client {
|
|
t.Helper()
|
|
cli, err := mautrix.NewClient(serverURL, "@alice:localhost", "token-test")
|
|
if err != nil {
|
|
t.Fatalf("NewClient: %v", err)
|
|
}
|
|
cli.UserID = id.UserID("@alice:localhost")
|
|
return cli
|
|
}
|
|
|
|
// TestMatrixSyncService_RecibeMensajeYStop arranca el servicio, recibe 1 evento y hace Stop limpio.
|
|
func TestMatrixSyncService_RecibeMensajeYStop(t *testing.T) {
|
|
srv := fakeSynapseServer(t, func(n int, w http.ResponseWriter, r *http.Request) {
|
|
if n == 1 {
|
|
_ = json.NewEncoder(w).Encode(syncRespMessage("nb_001"))
|
|
return
|
|
}
|
|
// Bloquear syncs subsiguientes hasta cancelacion
|
|
<-r.Context().Done()
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"next_batch": "nb_002"})
|
|
})
|
|
defer srv.Close()
|
|
|
|
cli := newTestSyncClient(t, srv.URL)
|
|
h, err := MatrixSyncService(context.Background(), MatrixSyncServiceConfig{
|
|
Client: cli,
|
|
ChannelBuffer: 16,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("MatrixSyncService: %v", err)
|
|
}
|
|
|
|
// Esperar el primer evento
|
|
select {
|
|
case ev, ok := <-h.Events:
|
|
if !ok {
|
|
t.Fatal("canal cerrado antes de recibir evento")
|
|
}
|
|
if ev.Type != "message" {
|
|
t.Errorf("tipo esperado 'message', got %q", ev.Type)
|
|
}
|
|
if ev.Body != "hola mundo" {
|
|
t.Errorf("body esperado 'hola mundo', got %q", ev.Body)
|
|
}
|
|
if ev.Sender != "@alice:localhost" {
|
|
t.Errorf("sender esperado '@alice:localhost', got %q", ev.Sender)
|
|
}
|
|
if ev.RoomID != "!testroom:localhost" {
|
|
t.Errorf("roomID esperado '!testroom:localhost', got %q", ev.RoomID)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("timeout esperando evento")
|
|
}
|
|
|
|
// Stop limpio
|
|
h.Stop()
|
|
|
|
// Verificar que Events cierra tras Stop
|
|
timeout := time.After(3 * time.Second)
|
|
for {
|
|
select {
|
|
case _, ok := <-h.Events:
|
|
if !ok {
|
|
return // canal cerrado correctamente
|
|
}
|
|
case <-timeout:
|
|
t.Fatal("canal Events no cerro tras Stop")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestMatrixSyncService_BackoffRecovery verifica backoff con 2 errores 500 seguidos de exito.
|
|
func TestMatrixSyncService_BackoffRecovery(t *testing.T) {
|
|
srv := fakeSynapseServer(t, func(n int, w http.ResponseWriter, r *http.Request) {
|
|
if n <= 2 {
|
|
// Primeras 2 llamadas: devolver 500
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"errcode": "M_UNKNOWN",
|
|
"error": "internal server error",
|
|
})
|
|
return
|
|
}
|
|
if n == 3 {
|
|
// Tercera llamada: respuesta correcta inmediata (no bloquear)
|
|
_ = json.NewEncoder(w).Encode(syncRespMessage("nb_recovery"))
|
|
return
|
|
}
|
|
// Cuarta en adelante: bloquear hasta cancelacion
|
|
<-r.Context().Done()
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"next_batch": "nb_x"})
|
|
})
|
|
defer srv.Close()
|
|
|
|
cli := newTestSyncClient(t, srv.URL)
|
|
h, err := MatrixSyncService(context.Background(), MatrixSyncServiceConfig{
|
|
Client: cli,
|
|
InitialBackoffMS: 50, // backoff corto para tests
|
|
MaxBackoffMS: 200,
|
|
ChannelBuffer: 16,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("MatrixSyncService: %v", err)
|
|
}
|
|
defer h.Stop()
|
|
|
|
// Tras los fallos, debe llegar el evento de recovery
|
|
select {
|
|
case ev, ok := <-h.Events:
|
|
if !ok {
|
|
t.Fatal("canal cerrado antes de evento de recovery")
|
|
}
|
|
if ev.Type != "message" {
|
|
t.Errorf("tipo esperado 'message', got %q", ev.Type)
|
|
}
|
|
if ev.Body != "hola mundo" {
|
|
t.Errorf("body esperado 'hola mundo', got %q", ev.Body)
|
|
}
|
|
case <-time.After(8 * time.Second):
|
|
t.Fatal("timeout esperando evento de recovery tras backoff")
|
|
}
|
|
}
|
|
|
|
// TestMatrixSyncService_Error401NoExit verifica que 401 emite error pero no cierra el servicio.
|
|
func TestMatrixSyncService_Error401NoExit(t *testing.T) {
|
|
srv := fakeSynapseServer(t, func(n int, w http.ResponseWriter, r *http.Request) {
|
|
if n == 1 {
|
|
// Primera llamada: 401 M_UNKNOWN_TOKEN
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"errcode": "M_UNKNOWN_TOKEN",
|
|
"error": "Invalid macaroon passed.",
|
|
})
|
|
return
|
|
}
|
|
// Bloquear: el servicio espera en backoff maximo
|
|
<-r.Context().Done()
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"next_batch": "nb_x"})
|
|
})
|
|
defer srv.Close()
|
|
|
|
cli := newTestSyncClient(t, srv.URL)
|
|
h, err := MatrixSyncService(context.Background(), MatrixSyncServiceConfig{
|
|
Client: cli,
|
|
InitialBackoffMS: 50,
|
|
MaxBackoffMS: 200,
|
|
ChannelBuffer: 8,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("MatrixSyncService: %v", err)
|
|
}
|
|
|
|
// Debe recibir al menos un error (fatal 401)
|
|
select {
|
|
case syncErr := <-h.Errors:
|
|
if syncErr == nil {
|
|
t.Error("error esperado no nil")
|
|
}
|
|
case <-time.After(4 * time.Second):
|
|
t.Fatal("timeout esperando error 401 en canal Errors")
|
|
}
|
|
|
|
// El canal Events NO debe estar cerrado — el servicio sigue activo
|
|
select {
|
|
case _, ok := <-h.Events:
|
|
if !ok {
|
|
t.Fatal("canal Events no debia cerrarse con error 401 (dejar al caller decidir via Stop)")
|
|
}
|
|
case <-time.After(300 * time.Millisecond):
|
|
// Correcto: canal sigue abierto
|
|
}
|
|
|
|
h.Stop()
|
|
|
|
// Tras Stop, Events debe cerrarse
|
|
select {
|
|
case _, ok := <-h.Events:
|
|
if !ok {
|
|
return // OK
|
|
}
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("canal Events no cerro tras Stop despues de error 401")
|
|
}
|
|
}
|
|
|
|
// TestMatrixSyncService_StopIdempotente verifica que Stop() dos veces no causa panic.
|
|
func TestMatrixSyncService_StopIdempotente(t *testing.T) {
|
|
srv := fakeSynapseServer(t, func(n int, w http.ResponseWriter, r *http.Request) {
|
|
<-r.Context().Done()
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"next_batch": "nb_1"})
|
|
})
|
|
defer srv.Close()
|
|
|
|
cli := newTestSyncClient(t, srv.URL)
|
|
h, err := MatrixSyncService(context.Background(), MatrixSyncServiceConfig{
|
|
Client: cli,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("MatrixSyncService: %v", err)
|
|
}
|
|
|
|
// Llamar Stop dos veces — no debe panic
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("Stop() dos veces causó panic: %v", r)
|
|
}
|
|
}()
|
|
h.Stop()
|
|
h.Stop()
|
|
}
|
|
|
|
// TestMatrixSyncService_CtxCancelCierraChannels verifica que cancelar ctx cierra Events < 1s.
|
|
func TestMatrixSyncService_CtxCancelCierraChannels(t *testing.T) {
|
|
srv := fakeSynapseServer(t, func(n int, w http.ResponseWriter, r *http.Request) {
|
|
<-r.Context().Done()
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"next_batch": "nb_ctx"})
|
|
})
|
|
defer srv.Close()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cli := newTestSyncClient(t, srv.URL)
|
|
h, err := MatrixSyncService(ctx, MatrixSyncServiceConfig{
|
|
Client: cli,
|
|
ChannelBuffer: 4,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("MatrixSyncService: %v", err)
|
|
}
|
|
|
|
// Cancelar contexto padre
|
|
cancel()
|
|
|
|
// Events debe cerrarse en menos de 1 segundo
|
|
deadline := time.After(1 * time.Second)
|
|
for {
|
|
select {
|
|
case _, ok := <-h.Events:
|
|
if !ok {
|
|
return // canal cerrado correctamente
|
|
}
|
|
case <-deadline:
|
|
t.Fatal("canal Events no cerro en 1s tras cancelar ctx")
|
|
}
|
|
}
|
|
}
|