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>
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
//go:build goolm || libolm
|
||||
|
||||
package matrix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// makeTestClient construye un *mautrix.Client apuntando al servidor dado con
|
||||
// credenciales validas para los tests.
|
||||
func makeTestClient(t *testing.T, serverURL string) *mautrix.Client {
|
||||
t.Helper()
|
||||
cli, err := mautrix.NewClient(serverURL, "@user:localhost", "test-token")
|
||||
if err != nil {
|
||||
t.Fatalf("mautrix.NewClient: %v", err)
|
||||
}
|
||||
cli.AccessToken = "test-token"
|
||||
cli.UserID = id.UserID("@user:localhost")
|
||||
cli.DeviceID = id.DeviceID("TESTDEVICE")
|
||||
return cli
|
||||
}
|
||||
|
||||
// validPickleKey genera una clave de 32 bytes para tests.
|
||||
func validPickleKey() []byte {
|
||||
key := make([]byte, 32)
|
||||
for i := range key {
|
||||
key[i] = byte(i + 1)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// newSynapseMock crea un httptest.Server que responde a los endpoints
|
||||
// necesarios para Init(): /keys/upload y /keys/query.
|
||||
// Acepta un statusCode para /keys/upload (200 = exito, 401 = token invalido).
|
||||
func newSynapseMock(t *testing.T, uploadStatus int) *httptest.Server {
|
||||
t.Helper()
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// POST /_matrix/client/v3/keys/upload -> one-time key counts
|
||||
mux.HandleFunc("/_matrix/client/v3/keys/upload", func(w http.ResponseWriter, r *http.Request) {
|
||||
if uploadStatus != http.StatusOK {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(uploadStatus)
|
||||
resp := map[string]any{
|
||||
"errcode": "M_UNKNOWN_TOKEN",
|
||||
"error": "Invalid access token",
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
resp := map[string]any{
|
||||
"one_time_key_counts": map[string]int{
|
||||
"signed_curve25519": 50,
|
||||
},
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
})
|
||||
|
||||
// POST /_matrix/client/v3/keys/query -> empty device keys
|
||||
mux.HandleFunc("/_matrix/client/v3/keys/query", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
resp := map[string]any{
|
||||
"device_keys": map[string]any{},
|
||||
"failures": map[string]any{},
|
||||
"master_keys": map[string]any{},
|
||||
"user_signing_keys": map[string]any{},
|
||||
"self_signing_keys": map[string]any{},
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
})
|
||||
|
||||
// GET /_matrix/client/v3/sync -> minimal empty sync
|
||||
mux.HandleFunc("/_matrix/client/v3/sync", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
resp := map[string]any{
|
||||
"next_batch": "s0_1",
|
||||
"rooms": map[string]any{},
|
||||
"to_device": map[string]any{"events": []any{}},
|
||||
"device_one_time_keys_count": map[string]any{},
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
})
|
||||
|
||||
// Catchall para no dejar requests colgados
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
})
|
||||
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func TestMatrixCryptoInit(t *testing.T) {
|
||||
t.Run("Client nil devuelve error", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
|
||||
Client: nil,
|
||||
StorePath: "/tmp/crypto_test.db",
|
||||
PickleKey: validPickleKey(),
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("esperaba error con Client nil, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Client no puede ser nil") {
|
||||
t.Errorf("mensaje de error inesperado: %q", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AccessToken vacio devuelve error", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cli, _ := mautrix.NewClient("http://localhost:8008", "@user:localhost", "")
|
||||
cli.UserID = "@user:localhost"
|
||||
cli.DeviceID = "DEVID"
|
||||
cli.AccessToken = ""
|
||||
_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
|
||||
Client: cli,
|
||||
StorePath: "/tmp/crypto_test.db",
|
||||
PickleKey: validPickleKey(),
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("esperaba error con AccessToken vacio, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UserID vacio devuelve error", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cli, _ := mautrix.NewClient("http://localhost:8008", "", "token_abc")
|
||||
cli.DeviceID = "DEVID"
|
||||
cli.AccessToken = "token_abc"
|
||||
cli.UserID = ""
|
||||
_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
|
||||
Client: cli,
|
||||
StorePath: "/tmp/crypto_test.db",
|
||||
PickleKey: validPickleKey(),
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("esperaba error con UserID vacio, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DeviceID vacio devuelve error", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cli, _ := mautrix.NewClient("http://localhost:8008", "@user:localhost", "token_abc")
|
||||
cli.AccessToken = "token_abc"
|
||||
cli.UserID = "@user:localhost"
|
||||
cli.DeviceID = ""
|
||||
_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
|
||||
Client: cli,
|
||||
StorePath: "/tmp/crypto_test.db",
|
||||
PickleKey: validPickleKey(),
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("esperaba error con DeviceID vacio, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("StorePath vacio devuelve error", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cli, _ := mautrix.NewClient("http://localhost:8008", "@user:localhost", "token")
|
||||
cli.AccessToken = "token"
|
||||
cli.UserID = "@user:localhost"
|
||||
cli.DeviceID = id.DeviceID("DEVID")
|
||||
_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
|
||||
Client: cli,
|
||||
StorePath: "",
|
||||
PickleKey: validPickleKey(),
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("esperaba error con StorePath vacio, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("StorePath relativo devuelve error", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cli, _ := mautrix.NewClient("http://localhost:8008", "@user:localhost", "token")
|
||||
cli.AccessToken = "token"
|
||||
cli.UserID = "@user:localhost"
|
||||
cli.DeviceID = id.DeviceID("DEVID")
|
||||
_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
|
||||
Client: cli,
|
||||
StorePath: "relative/path/crypto.db",
|
||||
PickleKey: validPickleKey(),
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("esperaba error con StorePath relativo, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PickleKey != 32 bytes devuelve error", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cli, _ := mautrix.NewClient("http://localhost:8008", "@user:localhost", "token")
|
||||
cli.AccessToken = "token"
|
||||
cli.UserID = "@user:localhost"
|
||||
cli.DeviceID = id.DeviceID("DEVID")
|
||||
// Clave de 16 bytes (demasiado corta)
|
||||
shortKey := make([]byte, 16)
|
||||
_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
|
||||
Client: cli,
|
||||
StorePath: "/tmp/crypto_test.db",
|
||||
PickleKey: shortKey,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("esperaba error con PickleKey de 16 bytes, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "32 bytes") {
|
||||
t.Errorf("mensaje de error debe mencionar '32 bytes', got %q", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("directorio del store se crea con permisos 0700", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storeDir := filepath.Join(tmpDir, "sub", "crypto_store")
|
||||
storePath := filepath.Join(storeDir, "crypto.db")
|
||||
|
||||
srv := newSynapseMock(t, http.StatusOK)
|
||||
defer srv.Close()
|
||||
|
||||
cli := makeTestClient(t, srv.URL)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// El Init puede fallar (e.g. sync loop), pero el directorio debe crearse.
|
||||
_, _ = MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
|
||||
Client: cli,
|
||||
StorePath: storePath,
|
||||
PickleKey: validPickleKey(),
|
||||
})
|
||||
|
||||
if _, statErr := os.Stat(storeDir); os.IsNotExist(statErr) {
|
||||
t.Fatalf("el directorio %q no fue creado", storeDir)
|
||||
}
|
||||
info, statErr := os.Stat(storeDir)
|
||||
if statErr != nil {
|
||||
t.Fatalf("no se pudo stat el directorio: %v", statErr)
|
||||
}
|
||||
perm := info.Mode().Perm()
|
||||
if perm != 0700 {
|
||||
t.Errorf("permisos del directorio: got %04o, want 0700", perm)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("input valido Init exito helper no nil", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := filepath.Join(tmpDir, "crypto.db")
|
||||
|
||||
srv := newSynapseMock(t, http.StatusOK)
|
||||
defer srv.Close()
|
||||
|
||||
cli := makeTestClient(t, srv.URL)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
|
||||
Client: cli,
|
||||
StorePath: storePath,
|
||||
PickleKey: validPickleKey(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("MatrixCryptoInit failed: %v", err)
|
||||
}
|
||||
if res == nil {
|
||||
t.Fatal("resultado es nil")
|
||||
}
|
||||
if res.Helper == nil {
|
||||
t.Fatal("Helper es nil")
|
||||
}
|
||||
if res.StorePath != storePath {
|
||||
t.Errorf("StorePath: got %q, want %q", res.StorePath, storePath)
|
||||
}
|
||||
if cli.Crypto == nil {
|
||||
t.Error("client.Crypto no fue asignado")
|
||||
}
|
||||
// Verificar que el archivo SQLite fue creado
|
||||
if _, err := os.Stat(storePath); os.IsNotExist(err) {
|
||||
t.Error("archivo crypto.db no fue creado")
|
||||
}
|
||||
if err := res.Helper.Close(); err != nil {
|
||||
t.Errorf("Helper.Close() error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Synapse 401 en keys upload devuelve error", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := filepath.Join(tmpDir, "crypto.db")
|
||||
|
||||
srv := newSynapseMock(t, http.StatusUnauthorized)
|
||||
defer srv.Close()
|
||||
|
||||
cli := makeTestClient(t, srv.URL)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
|
||||
Client: cli,
|
||||
StorePath: storePath,
|
||||
PickleKey: validPickleKey(),
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("esperaba error con Synapse 401, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "helper.Init failed") {
|
||||
t.Errorf("mensaje de error inesperado: %q", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user