package hyperswitch import ( "context" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func setupWebhookLogDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&WebhookLog{})) return db } func TestLogWebhook_PersistsMinimalFields(t *testing.T) { db := setupWebhookLogDB(t) row := &WebhookLog{ Payload: `{"event_type":"payment.succeeded","payment_id":"pay_1"}`, SignatureValid: true, SignatureHeader: "deadbeef", ProcessingResult: "ok", SourceIP: "203.0.113.7", UserAgent: "Hyperswitch/1.0", RequestID: "req_abc", } require.NoError(t, LogWebhook(context.Background(), db, row)) var persisted WebhookLog require.NoError(t, db.First(&persisted, row.ID).Error) assert.Equal(t, row.Payload, persisted.Payload) assert.True(t, persisted.SignatureValid) assert.Equal(t, "ok", persisted.ProcessingResult) assert.Equal(t, "203.0.113.7", persisted.SourceIP) assert.Equal(t, "Hyperswitch/1.0", persisted.UserAgent) assert.Equal(t, "req_abc", persisted.RequestID) // event_type is extracted from the payload on insert — the caller // didn't populate it, LogWebhook did. assert.Equal(t, "payment.succeeded", persisted.EventType) // received_at auto-populated assert.False(t, persisted.ReceivedAt.IsZero()) // Explicit non-nil ID assert.NotEqual(t, uuid.Nil, persisted.ID) } func TestLogWebhook_FillsMissingRequestID(t *testing.T) { db := setupWebhookLogDB(t) row := &WebhookLog{ Payload: `{}`, SignatureValid: false, ProcessingResult: "signature_invalid", // RequestID left empty — LogWebhook must generate one. } require.NoError(t, LogWebhook(context.Background(), db, row)) assert.NotEmpty(t, row.RequestID) _, err := uuid.Parse(row.RequestID) assert.NoError(t, err, "generated request_id must be a valid UUID") } func TestLogWebhook_InvalidJSONLeavesEventTypeEmpty(t *testing.T) { db := setupWebhookLogDB(t) row := &WebhookLog{ Payload: `not json at all`, SignatureValid: false, ProcessingResult: "signature_invalid", RequestID: "req_probe", } require.NoError(t, LogWebhook(context.Background(), db, row)) var persisted WebhookLog require.NoError(t, db.First(&persisted, row.ID).Error) // Attack probes / malformed payloads: event_type stays empty, no // insert failure — the row exists for forensics regardless. assert.Empty(t, persisted.EventType) assert.Equal(t, "not json at all", persisted.Payload) } func TestLogWebhook_CapturesInvalidSignatureRows(t *testing.T) { db := setupWebhookLogDB(t) // The point of the log: even rejected deliveries persist. Drive // the insert the way the handler would on a signature failure. row := &WebhookLog{ Payload: `{"fake":"payload"}`, SignatureValid: false, SignatureHeader: "invalid-sig", ProcessingResult: "signature_invalid", SourceIP: "198.51.100.42", RequestID: "req_attack", } require.NoError(t, LogWebhook(context.Background(), db, row)) var count int64 require.NoError(t, db.Model(&WebhookLog{}). Where("signature_valid = ? AND source_ip = ?", false, "198.51.100.42"). Count(&count).Error) assert.Equal(t, int64(1), count, "forensics query on signature_invalid rows must find the attack attempt") } func TestExtractEventType_Variants(t *testing.T) { cases := []struct { name string payload string want string }{ {"valid event", `{"event_type":"refund.succeeded"}`, "refund.succeeded"}, {"extra fields", `{"payment_id":"x","event_type":"payment.processing","amount":500}`, "payment.processing"}, {"missing field", `{"payment_id":"x"}`, ""}, {"empty payload", "", ""}, {"not json", `foo`, ""}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert.Equal(t, tc.want, extractEventType(tc.payload)) }) } }