mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 18:26:16 +00:00
feat: isolate manual verification RPC usage
- add rpc node purpose support for general/manual_verify/both - keep scan, WSS and health checks away from manual_verify nodes - fallback manual hash verification from general RPC to manual_verify RPC - add public cashier hash submission path and errno responses - update tests for RPC selection, failover and manual payment flows
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/GMWalletApp/epusdt/internal/testutil"
|
||||
@@ -99,3 +100,306 @@ func TestSelectRpcNodeIgnoresDownRows(t *testing.T) {
|
||||
t.Fatalf("SelectRpcNode() = %#v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectRpcNodeExcludesManualVerifyRows(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := dao.Mdb.Create(&mdb.RpcNode{
|
||||
Network: mdb.NetworkTron,
|
||||
Url: "https://manual.example.com",
|
||||
Type: mdb.RpcNodeTypeHttp,
|
||||
Weight: 100,
|
||||
Enabled: true,
|
||||
Purpose: mdb.RpcNodePurposeManualVerify,
|
||||
Status: mdb.RpcNodeStatusOk,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed manual rpc_node: %v", err)
|
||||
}
|
||||
if err := dao.Mdb.Create(&mdb.RpcNode{
|
||||
Network: mdb.NetworkTron,
|
||||
Url: "https://general.example.com",
|
||||
Type: mdb.RpcNodeTypeHttp,
|
||||
Weight: 1,
|
||||
Enabled: true,
|
||||
Purpose: mdb.RpcNodePurposeGeneral,
|
||||
Status: mdb.RpcNodeStatusUnknown,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed general rpc_node: %v", err)
|
||||
}
|
||||
|
||||
got, err := SelectRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectRpcNode(): %v", err)
|
||||
}
|
||||
if got == nil || got.Url != "https://general.example.com" {
|
||||
t.Fatalf("SelectRpcNode() = %#v, want general row", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectRpcNodePurposeFilterAppliesToAnyNodeType(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
const nodeType = "grpc"
|
||||
if err := dao.Mdb.Create(&mdb.RpcNode{
|
||||
Network: mdb.NetworkTron,
|
||||
Url: "grpc://manual.example.com",
|
||||
Type: nodeType,
|
||||
Weight: 100,
|
||||
Enabled: true,
|
||||
Purpose: mdb.RpcNodePurposeManualVerify,
|
||||
Status: mdb.RpcNodeStatusOk,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed manual rpc_node: %v", err)
|
||||
}
|
||||
if err := dao.Mdb.Create(&mdb.RpcNode{
|
||||
Network: mdb.NetworkTron,
|
||||
Url: "grpc://general.example.com",
|
||||
Type: nodeType,
|
||||
Weight: 1,
|
||||
Enabled: true,
|
||||
Purpose: mdb.RpcNodePurposeGeneral,
|
||||
Status: mdb.RpcNodeStatusOk,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed general rpc_node: %v", err)
|
||||
}
|
||||
|
||||
got, err := SelectRpcNode(mdb.NetworkTron, nodeType)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectRpcNode(): %v", err)
|
||||
}
|
||||
if got == nil || got.Url != "grpc://general.example.com" {
|
||||
t.Fatalf("SelectRpcNode() = %#v, want general grpc row", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectRpcNodeManualVerifyOnlyReturnsNil(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := dao.Mdb.Create(&mdb.RpcNode{
|
||||
Network: mdb.NetworkTron,
|
||||
Url: "https://manual-only.example.com",
|
||||
Type: mdb.RpcNodeTypeHttp,
|
||||
Weight: 1,
|
||||
Enabled: true,
|
||||
Purpose: mdb.RpcNodePurposeManualVerify,
|
||||
Status: mdb.RpcNodeStatusOk,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed manual rpc_node: %v", err)
|
||||
}
|
||||
|
||||
got, err := SelectRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectRpcNode(): %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("SelectRpcNode() = %#v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListManualPaymentRpcCandidatesGeneralBeforeManualVerify(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
rows := []mdb.RpcNode{
|
||||
{Network: mdb.NetworkTron, Url: "https://manual-ok.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusOk},
|
||||
{Network: mdb.NetworkTron, Url: "https://general-unknown.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
|
||||
{Network: mdb.NetworkTron, Url: "https://general-ok.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
|
||||
{Network: mdb.NetworkTron, Url: "https://manual-unknown.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusUnknown},
|
||||
{Network: mdb.NetworkTron, Url: "https://down.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusDown},
|
||||
}
|
||||
if err := dao.Mdb.Create(&rows).Error; err != nil {
|
||||
t.Fatalf("seed rpc_nodes: %v", err)
|
||||
}
|
||||
|
||||
got, err := ListManualPaymentRpcCandidates(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
|
||||
if err != nil {
|
||||
t.Fatalf("ListManualPaymentRpcCandidates(): %v", err)
|
||||
}
|
||||
var urls []string
|
||||
for _, row := range got {
|
||||
urls = append(urls, row.Url)
|
||||
}
|
||||
want := []string{
|
||||
"https://general-ok.example.com",
|
||||
"https://general-unknown.example.com",
|
||||
"https://manual-ok.example.com",
|
||||
"https://manual-unknown.example.com",
|
||||
}
|
||||
if len(urls) != len(want) {
|
||||
t.Fatalf("candidate urls = %#v, want %#v", urls, want)
|
||||
}
|
||||
for i := range want {
|
||||
if urls[i] != want[i] {
|
||||
t.Fatalf("candidate urls = %#v, want %#v", urls, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListManualPaymentRpcCandidatesAllowsManualVerifyFallbackOnly(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
rows := []mdb.RpcNode{
|
||||
{Network: mdb.NetworkTron, Url: "https://manual-ok.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusOk},
|
||||
{Network: mdb.NetworkTron, Url: "https://manual-down.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusDown},
|
||||
}
|
||||
if err := dao.Mdb.Create(&rows).Error; err != nil {
|
||||
t.Fatalf("seed rpc_nodes: %v", err)
|
||||
}
|
||||
|
||||
got, err := ListManualPaymentRpcCandidates(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
|
||||
if err != nil {
|
||||
t.Fatalf("ListManualPaymentRpcCandidates(): %v", err)
|
||||
}
|
||||
if len(got) != 1 || got[0].Url != "https://manual-ok.example.com" {
|
||||
t.Fatalf("candidate rows = %#v, want only manual ok fallback", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectGeneralRpcNodeSkipsCoolingNode(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
ResetRpcFailoverForTest()
|
||||
t.Cleanup(ResetRpcFailoverForTest)
|
||||
|
||||
rows := []mdb.RpcNode{
|
||||
{Network: mdb.NetworkTron, Url: "https://primary.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
|
||||
{Network: mdb.NetworkTron, Url: "https://backup.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
|
||||
}
|
||||
if err := dao.Mdb.Create(&rows).Error; err != nil {
|
||||
t.Fatalf("seed rpc_nodes: %v", err)
|
||||
}
|
||||
|
||||
first, err := SelectGeneralRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectGeneralRpcNode(): %v", err)
|
||||
}
|
||||
if first == nil || first.Url != "https://primary.example.com" {
|
||||
t.Fatalf("first node = %#v, want primary", first)
|
||||
}
|
||||
|
||||
for i := 0; i < RpcFailoverThreshold; i++ {
|
||||
RecordRpcNodeFailure(first.ID)
|
||||
}
|
||||
|
||||
got, err := SelectGeneralRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectGeneralRpcNode(): %v", err)
|
||||
}
|
||||
if got == nil || got.Url != "https://backup.example.com" {
|
||||
t.Fatalf("node after cooldown = %#v, want backup", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectGeneralRpcNodeFallsBackWhenAllNodesCooling(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
ResetRpcFailoverForTest()
|
||||
t.Cleanup(ResetRpcFailoverForTest)
|
||||
|
||||
rows := []mdb.RpcNode{
|
||||
{Network: mdb.NetworkTron, Url: "https://primary.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
|
||||
{Network: mdb.NetworkTron, Url: "https://backup.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
|
||||
}
|
||||
if err := dao.Mdb.Create(&rows).Error; err != nil {
|
||||
t.Fatalf("seed rpc_nodes: %v", err)
|
||||
}
|
||||
for i := range rows {
|
||||
for n := 0; n < RpcFailoverThreshold; n++ {
|
||||
RecordRpcNodeFailure(rows[i].ID)
|
||||
}
|
||||
}
|
||||
|
||||
got, err := SelectGeneralRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectGeneralRpcNode(): %v", err)
|
||||
}
|
||||
if got == nil || got.Url != "https://primary.example.com" {
|
||||
t.Fatalf("SelectGeneralRpcNode() = %#v, want primary when every node is cooling", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectGeneralRpcNodeReturnsNilWhenExcludedAlternatesCooling(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
ResetRpcFailoverForTest()
|
||||
t.Cleanup(ResetRpcFailoverForTest)
|
||||
|
||||
rows := []mdb.RpcNode{
|
||||
{Network: mdb.NetworkTron, Url: "https://primary.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
|
||||
{Network: mdb.NetworkTron, Url: "https://backup.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
|
||||
}
|
||||
if err := dao.Mdb.Create(&rows).Error; err != nil {
|
||||
t.Fatalf("seed rpc_nodes: %v", err)
|
||||
}
|
||||
for i := 0; i < RpcFailoverThreshold; i++ {
|
||||
RecordRpcNodeFailure(rows[1].ID)
|
||||
}
|
||||
|
||||
got, err := SelectGeneralRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp, rows[0].ID)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectGeneralRpcNode(): %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("SelectGeneralRpcNode() = %#v, want nil when only alternate is cooling", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordRpcNodeSuccessClearsCooldown(t *testing.T) {
|
||||
ResetRpcFailoverForTest()
|
||||
t.Cleanup(ResetRpcFailoverForTest)
|
||||
|
||||
const nodeID uint64 = 42
|
||||
for i := 0; i < RpcFailoverThreshold; i++ {
|
||||
RecordRpcNodeFailure(nodeID)
|
||||
}
|
||||
if !IsRpcNodeCoolingDown(nodeID) {
|
||||
t.Fatal("node should be cooling after threshold")
|
||||
}
|
||||
RecordRpcNodeSuccess(nodeID)
|
||||
if IsRpcNodeCoolingDown(nodeID) {
|
||||
t.Fatal("node should not be cooling after success")
|
||||
}
|
||||
failures, cooling := RecordRpcNodeFailure(nodeID)
|
||||
if cooling || failures != 1 {
|
||||
t.Fatalf("failure state after success = failures=%d cooling=%v, want failures=1 cooling=false", failures, cooling)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRpcNodeLogLabelHidesPathAndQuery(t *testing.T) {
|
||||
label := RpcNodeLogLabel(mdb.RpcNode{
|
||||
BaseModel: mdb.BaseModel{ID: 7},
|
||||
Network: mdb.NetworkEthereum,
|
||||
Type: mdb.RpcNodeTypeWs,
|
||||
Url: "wss://rpc.example.com/v3/secret-key?token=secret",
|
||||
})
|
||||
if strings.Contains(label, "secret") || strings.Contains(label, "/v3/") {
|
||||
t.Fatalf("RpcNodeLogLabel() leaked sensitive URL parts: %s", label)
|
||||
}
|
||||
if !strings.Contains(label, "wss://rpc.example.com") {
|
||||
t.Fatalf("RpcNodeLogLabel() = %s, want host", label)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRpcNodesForHealthExcludesManualVerify(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
rows := []mdb.RpcNode{
|
||||
{Network: mdb.NetworkTron, Url: "https://general.example.com", Type: mdb.RpcNodeTypeHttp, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
|
||||
{Network: mdb.NetworkTron, Url: "https://manual.example.com", Type: mdb.RpcNodeTypeHttp, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusUnknown},
|
||||
}
|
||||
if err := dao.Mdb.Create(&rows).Error; err != nil {
|
||||
t.Fatalf("seed rpc_nodes: %v", err)
|
||||
}
|
||||
got, err := ListRpcNodesForHealth()
|
||||
if err != nil {
|
||||
t.Fatalf("ListRpcNodesForHealth(): %v", err)
|
||||
}
|
||||
if len(got) != 1 || got[0].Url != "https://general.example.com" {
|
||||
t.Fatalf("ListRpcNodesForHealth() = %#v, want only general row", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user