621e8895c9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
322 lines
9.3 KiB
Go
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())
|
|
}
|
|
})
|
|
}
|