package services import ( "os" "strings" "testing" "time" ) // El bug real: salt y timestamp estaban fijos en el código // (salt "fixed_salt_12", timestamp 1747194248 = mayo 2025). Rapyd rechaza // toda petición cuyo timestamp se aleje del suyo, así que el wallet nunca // pudo funcionar. Y un salt fijo anula la protección contra reenvío. func TestSaltEsDistintoCadaVez(t *testing.T) { vistos := map[string]bool{} for i := 0; i < 200; i++ { s, err := generarSalt() if err != nil { t.Fatalf("generarSalt: %v", err) } if len(s) != 12 { t.Fatalf("largo %d, esperaba 12", len(s)) } if vistos[s] { t.Fatalf("salt repetido en 200 intentos: %q", s) } vistos[s] = true for _, r := range s { if !strings.ContainsRune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", r) { t.Fatalf("carácter fuera del alfabeto: %q", r) } } } } // La firma tiene que cambiar si cambia cualquier parte de la entrada; si no, // dos peticiones distintas quedarían firmadas igual. func TestFirmaDependeDeCadaComponente(t *testing.T) { cred := &rapydCreds{baseURL: "https://x", accessKey: "rak_test", secretKey: "rsk_test"} base := firmaRapyd(cred, "post", "/v1/ewallets", []byte(`{"a":1}`), "salt1", 1700000000) casos := map[string]string{ "otro método": firmaRapyd(cred, "get", "/v1/ewallets", []byte(`{"a":1}`), "salt1", 1700000000), "otra ruta": firmaRapyd(cred, "post", "/v1/otros", []byte(`{"a":1}`), "salt1", 1700000000), "otro body": firmaRapyd(cred, "post", "/v1/ewallets", []byte(`{"a":2}`), "salt1", 1700000000), "otro salt": firmaRapyd(cred, "post", "/v1/ewallets", []byte(`{"a":1}`), "salt2", 1700000000), "otro timestamp": firmaRapyd(cred, "post", "/v1/ewallets", []byte(`{"a":1}`), "salt1", 1700000001), } for nombre, f := range casos { if f == base { t.Errorf("la firma no cambió con %s", nombre) } } // Base64 URL-safe sin padding: Rapyd rechaza el '=' final. if strings.Contains(base, "=") || strings.Contains(base, "+") || strings.Contains(base, "/") { t.Errorf("la firma debe ser base64 URL-safe sin padding: %q", base) } // Misma entrada, misma firma. if firmaRapyd(cred, "post", "/v1/ewallets", []byte(`{"a":1}`), "salt1", 1700000000) != base { t.Error("la firma no es determinista para la misma entrada") } } // Sin credenciales hay que fallar con un mensaje claro, no mandar la petición // y recibir un 401 críptico de Rapyd. func TestRapydConfigFallaSinCredenciales(t *testing.T) { for _, k := range []string{"RAPYD_ACCESS_KEY", "RAPYD_SECRET_KEY", "RAPYD_BASE_URL"} { t.Setenv(k, "") } if _, err := rapydConfig(); err == nil { t.Fatal("esperaba error sin credenciales") } t.Setenv("RAPYD_ACCESS_KEY", "rak_x") t.Setenv("RAPYD_SECRET_KEY", "rsk_x") cred, err := rapydConfig() if err != nil { t.Fatalf("no esperaba error: %v", err) } if cred.baseURL != rapydURLPorDefecto { t.Errorf("baseURL = %q, esperaba el default", cred.baseURL) } t.Setenv("RAPYD_BASE_URL", "https://api.rapyd.net/") cred, _ = rapydConfig() if cred.baseURL != "https://api.rapyd.net" { t.Errorf("no se quitó la barra final: %q", cred.baseURL) } } // El timestamp tiene que ser el de ahora. Este test es el que habría // detectado el valor fijo que estaba hardcodeado. func TestTimestampEsActual(t *testing.T) { os.Unsetenv("RAPYD_BASE_URL") ahora := time.Now().Unix() const mayo2025 = int64(1747194248) if ahora-mayo2025 < 0 { t.Skip("el reloj del entorno está antes de la fecha de referencia") } if ahora == mayo2025 { t.Fatal("el timestamp sigue siendo el valor fijo") } }