diff --git a/go.mod b/go.mod index 20483c5..9e2452e 100644 --- a/go.mod +++ b/go.mod @@ -36,3 +36,5 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace gopkg.in/dnaeon/go-vcr.v4 => github.com/dnaeon/go-vcr/v4 v4.0.5 diff --git a/go.sum b/go.sum index 6cfbb9e..2e124a8 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr/v4 v4.0.5 h1:mHtf8dT8cvBT6n4YZ3zwOmqFGxHZppgitHUU9l5FZLo= +github.com/dnaeon/go-vcr/v4 v4.0.5/go.mod h1:dRos81TkW9C1WJt6tTaE+uV2Lo8qJT3AG2b35+CB/nQ= github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= @@ -173,8 +175,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/dnaeon/go-vcr.v4 v4.0.5 h1:I0hpTIvD5rII+8LgYGrHMA2d4SQPoL6u7ZvJakWKsiA= -gopkg.in/dnaeon/go-vcr.v4 v4.0.5/go.mod h1:dRos81TkW9C1WJt6tTaE+uV2Lo8qJT3AG2b35+CB/nQ= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/server/server.go b/server/server.go index c17eb5f..865bb38 100644 --- a/server/server.go +++ b/server/server.go @@ -20,6 +20,9 @@ type TestServer struct { bots map[int]*tcmock.Bot deals map[int]*tcmock.Deal + // Configuration + allowDuplicateIDs bool + // Error simulation rateLimitEnabled bool rateLimitRetry int @@ -63,6 +66,16 @@ func (ts *TestServer) Reset() { ts.botErrors = make(map[int]error) ts.dealErrors = make(map[int]error) ts.rateLimitEnabled = false + ts.allowDuplicateIDs = false +} + +// AllowDuplicateIDs enables or disables duplicate ID checking +// When enabled, loading VCR cassettes with duplicate IDs will not return an error +// Instead, duplicate entries will be skipped (existing entries are preserved) +func (ts *TestServer) AllowDuplicateIDs(allow bool) { + ts.mu.Lock() + defer ts.mu.Unlock() + ts.allowDuplicateIDs = allow } // ListBots implements the ServerInterface method for GET /ver1/bots diff --git a/server/vcr.go b/server/vcr.go index 0275da4..0ba3b00 100644 --- a/server/vcr.go +++ b/server/vcr.go @@ -21,7 +21,7 @@ var ( // LoadVCRCassette loads a VCR cassette and populates mock server state // - Bots and Deals are loaded with ALL their data from real API responses // - bot_events are PRESERVED exactly as recorded (this is the valuable part!) -// - Duplicate IDs will return an error +// - Duplicate IDs will return an error unless AllowDuplicateIDs(true) is set // - Non-2xx responses are skipped func (ts *TestServer) LoadVCRCassette(cassettePath string) error { // Load cassette from file @@ -94,12 +94,18 @@ func (ts *TestServer) loadDealFromJSON(jsonBody string) error { // Check for duplicate ts.mu.RLock() - if _, exists := ts.deals[deal.Id]; exists { - ts.mu.RUnlock() - return fmt.Errorf("duplicate deal ID %d found in VCR cassette", deal.Id) - } + _, exists := ts.deals[deal.Id] + allowDuplicates := ts.allowDuplicateIDs ts.mu.RUnlock() + if exists { + if !allowDuplicates { + return fmt.Errorf("duplicate deal ID %d found in VCR cassette", deal.Id) + } + // Skip this deal if duplicates are allowed (preserve existing entry) + return nil + } + // Check if bot exists, create a minimal one if not ts.mu.RLock() botExists := ts.bots[deal.BotId] != nil @@ -130,12 +136,18 @@ func (ts *TestServer) loadDealsListFromJSON(jsonBody string) error { for _, deal := range deals { // Check for duplicate ts.mu.RLock() - if _, exists := ts.deals[deal.Id]; exists { - ts.mu.RUnlock() - return fmt.Errorf("duplicate deal ID %d found in VCR cassette", deal.Id) - } + _, exists := ts.deals[deal.Id] + allowDuplicates := ts.allowDuplicateIDs ts.mu.RUnlock() + if exists { + if !allowDuplicates { + return fmt.Errorf("duplicate deal ID %d found in VCR cassette", deal.Id) + } + // Skip this deal if duplicates are allowed (preserve existing entry) + continue + } + // Check if bot exists, create a minimal one if not ts.mu.RLock() botExists := ts.bots[deal.BotId] != nil @@ -168,12 +180,18 @@ func (ts *TestServer) loadBotsListFromJSON(jsonBody string) error { for _, bot := range bots { // Check for duplicate ts.mu.RLock() - if _, exists := ts.bots[bot.Id]; exists { - ts.mu.RUnlock() - return fmt.Errorf("duplicate bot ID %d found in VCR cassette", bot.Id) - } + _, exists := ts.bots[bot.Id] + allowDuplicates := ts.allowDuplicateIDs ts.mu.RUnlock() + if exists { + if !allowDuplicates { + return fmt.Errorf("duplicate bot ID %d found in VCR cassette", bot.Id) + } + // Skip this bot if duplicates are allowed (preserve existing entry) + continue + } + // Add bot with all its data ts.mu.Lock() botCopy := bot // Create a copy to get the correct pointer diff --git a/server/vcr_test.go b/server/vcr_test.go index c629f49..3b4b30e 100644 --- a/server/vcr_test.go +++ b/server/vcr_test.go @@ -102,6 +102,44 @@ func TestLoadVCRCassette_DuplicateError(t *testing.T) { } } +func TestLoadVCRCassette_AllowDuplicates(t *testing.T) { + ts := NewTestServer(t) + defer ts.Close() + + // Enable duplicate ID allowance + ts.AllowDuplicateIDs(true) + + // Load cassette first time - should succeed + err := ts.LoadVCRCassette("../testdata/fixtures/deal_2376446537") + if err != nil { + t.Fatalf("first load failed: %v", err) + } + + // Verify the deal was loaded + deal, ok := ts.GetDealByID(2376446537) + if !ok { + t.Fatal("deal 2376446537 not found after first load") + } + if deal.Pair != "USDT_DOGE" { + t.Errorf("expected pair USDT_DOGE, got %s", deal.Pair) + } + + // Load same cassette again - should succeed (duplicates allowed) + err = ts.LoadVCRCassette("../testdata/fixtures/deal_2376446537") + if err != nil { + t.Fatalf("second load failed with AllowDuplicateIDs enabled: %v", err) + } + + // Verify the original deal is still there and unchanged + deal, ok = ts.GetDealByID(2376446537) + if !ok { + t.Fatal("deal 2376446537 not found after second load") + } + if deal.Pair != "USDT_DOGE" { + t.Errorf("expected pair USDT_DOGE after second load, got %s", deal.Pair) + } +} + func TestLoadVCRCassettes_Multiple(t *testing.T) { ts := NewTestServer(t) defer ts.Close()