//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()) } }) }