Files
fn_registry/functions/infra/matrix_crypto_init_test.go
T
egutierrez 621e8895c9 feat(infra): auto-commit con 86 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:38:15 +02:00

322 lines
9.3 KiB
Go

//go:build goolm || libolm
package infra
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())
}
})
}