package webhooks_test import ( "net/http" "strconv" "testing" "time" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/option" ) // Standardized test constants (matches TypeScript implementation) const ( testPayload = `{"id": "evt_685c059ae3a481909bdc86819b066fb6", "object": "event", "created_at": 1750861210, "type": "response.completed", "data": {"id": "resp_123"}}` testSecret = "whsec_RdvaYFYUXuIFuEbvZHwMfYFhUf7aMYjYcmM24+Aj40c=" testTimestamp = 1750861210 testWebhookID = "wh_685c059ae39c8190af8c71ed1022a24d" testSignature = "v1,gUAg4R2hWouRZqRQG4uJypNS8YK885G838+EHb4nKBY=" ) // Helper function to create test headers with standardized timestamp func createTestHeaders() http.Header { return http.Header{ "Webhook-Signature": []string{testSignature}, "Webhook-Timestamp": []string{strconv.FormatInt(testTimestamp, 10)}, "Webhook-Id": []string{testWebhookID}, } } func TestWebhookService_VerifySignature_ValidSignature(t *testing.T) { client := openai.NewClient( option.WithAPIKey("test-key"), option.WithWebhookSecret(testSecret), ) // Use the time-aware method with the fixed timestamp for testing fixedTime := time.Unix(testTimestamp, 0) err := client.Webhooks.VerifySignatureWithToleranceAndTime([]byte(testPayload), createTestHeaders(), 5*time.Minute, fixedTime) if err != nil { t.Errorf("VerifySignatureWithToleranceAndTime should have succeeded with valid signature: %v", err) } } func TestWebhookService_VerifySignature_InvalidSignature(t *testing.T) { client := openai.NewClient( option.WithAPIKey("test-key"), option.WithWebhookSecret(testSecret), ) // Create headers with an invalid signature invalidHeaders := http.Header{ "Webhook-Signature": []string{"v1,invalid_signature_here"}, "Webhook-Timestamp": []string{strconv.FormatInt(testTimestamp, 10)}, "Webhook-Id": []string{testWebhookID}, } // Use time-aware method to avoid timestamp issues fixedTime := time.Unix(testTimestamp, 0) err := client.Webhooks.VerifySignatureWithToleranceAndTime([]byte(testPayload), invalidHeaders, 5*time.Minute, fixedTime) if err == nil { t.Error("VerifySignatureWithToleranceAndTime should have failed with invalid signature") } expectedError := "webhook signature verification failed" if err.Error() != expectedError { t.Errorf("Expected error '%s', got '%s'", expectedError, err.Error()) } } func TestWebhookService_VerifySignature_MissingSecret(t *testing.T) { client := openai.NewClient( option.WithAPIKey("test-key"), ) err := client.Webhooks.VerifySignature([]byte(testPayload), createTestHeaders()) if err == nil { t.Error("VerifySignature should have failed with missing secret") } expectedError := "webhook secret must be provided either in the method call or configured on the client" if err.Error() != expectedError { t.Errorf("Expected error '%s', got '%s'", expectedError, err.Error()) } } func TestWebhookService_VerifySignature_InvalidSignatureForInvalidTimestamp(t *testing.T) { client := openai.NewClient( option.WithAPIKey("test-key"), option.WithWebhookSecret(testSecret), ) invalidHeaders := http.Header{ "Webhook-Signature": []string{"v1,signature"}, "Webhook-Timestamp": []string{"invalid"}, "Webhook-Id": []string{testWebhookID}, } err := client.Webhooks.VerifySignature([]byte(testPayload), invalidHeaders) if err == nil { t.Error("VerifySignature should have failed with invalid timestamp") } // Should fail on timestamp format validation first expectedError := "invalid webhook timestamp format" if err.Error() != expectedError { t.Errorf("Expected error '%s', got '%s'", expectedError, err.Error()) } } func TestWebhookService_VerifySignature_SignatureWithoutV1Prefix(t *testing.T) { client := openai.NewClient( option.WithAPIKey("test-key"), option.WithWebhookSecret(testSecret), ) currentTimestamp := strconv.FormatInt(testTimestamp, 10) headersWithoutV1 := http.Header{ "Webhook-Signature": []string{"9WlByKQUfBVM08XRYmo3WqR/dQXtjGJkV1edShZZ+C0="}, "Webhook-Timestamp": []string{currentTimestamp}, "Webhook-Id": []string{testWebhookID}, } // Use time-aware method to avoid timestamp issues fixedTime := time.Unix(testTimestamp, 0) err := client.Webhooks.VerifySignatureWithToleranceAndTime([]byte(testPayload), headersWithoutV1, 5*time.Minute, fixedTime) if err == nil { t.Error("VerifySignatureWithToleranceAndTime should have failed on signature verification") } // Should fail on signature verification expectedError := "webhook signature verification failed" if err.Error() != expectedError { t.Errorf("Expected error '%s', got '%s'", expectedError, err.Error()) } } func TestWebhookService_VerifySignature_TimestampTooOld(t *testing.T) { client := openai.NewClient( option.WithAPIKey("test-key"), option.WithWebhookSecret(testSecret), ) // Use a timestamp that's older than 5 minutes oldTimestamp := strconv.FormatInt(testTimestamp-400, 10) // 6 minutes 40 seconds ago headersOld := http.Header{ "Webhook-Signature": []string{"v1,signature"}, "Webhook-Timestamp": []string{oldTimestamp}, "Webhook-Id": []string{testWebhookID}, } // Use the current time for comparison - this should fail because the timestamp is too old currentTime := time.Unix(testTimestamp, 0) err := client.Webhooks.VerifySignatureWithToleranceAndTime([]byte(testPayload), headersOld, 5*time.Minute, currentTime) if err == nil { t.Error("VerifySignatureWithToleranceAndTime should have failed with old timestamp") } expectedError := "webhook timestamp is too old" if err.Error() != expectedError { t.Errorf("Expected error '%s', got '%s'", expectedError, err.Error()) } } func TestWebhookService_VerifySignature_TimestampTooNew(t *testing.T) { client := openai.NewClient( option.WithAPIKey("test-key"), option.WithWebhookSecret(testSecret), ) // Use a timestamp that's in the future beyond tolerance futureTimestamp := strconv.FormatInt(testTimestamp+400, 10) // 6 minutes 40 seconds in the future headersFuture := http.Header{ "Webhook-Signature": []string{"v1,signature"}, "Webhook-Timestamp": []string{futureTimestamp}, "Webhook-Id": []string{testWebhookID}, } // Use the current time for comparison - this should fail because the timestamp is too new currentTime := time.Unix(testTimestamp, 0) err := client.Webhooks.VerifySignatureWithToleranceAndTime([]byte(testPayload), headersFuture, 5*time.Minute, currentTime) if err == nil { t.Error("VerifySignatureWithToleranceAndTime should have failed with future timestamp") } expectedError := "webhook timestamp is too new" if err.Error() != expectedError { t.Errorf("Expected error '%s', got '%s'", expectedError, err.Error()) } } func TestWebhookService_VerifySignature_InvalidTimestampFormat(t *testing.T) { client := openai.NewClient( option.WithAPIKey("test-key"), option.WithWebhookSecret(testSecret), ) headersInvalid := http.Header{ "Webhook-Signature": []string{"v1,signature"}, "Webhook-Timestamp": []string{"not_a_number"}, "Webhook-Id": []string{testWebhookID}, } err := client.Webhooks.VerifySignature([]byte(testPayload), headersInvalid) if err == nil { t.Error("VerifySignature should have failed with invalid timestamp format") } expectedError := "invalid webhook timestamp format" if err.Error() != expectedError { t.Errorf("Expected error '%s', got '%s'", expectedError, err.Error()) } } func TestWebhookService_VerifySignatureWithTolerance_CustomTolerance(t *testing.T) { client := openai.NewClient( option.WithAPIKey("test-key"), option.WithWebhookSecret(testSecret), ) // Use a timestamp that's older than default tolerance but within custom tolerance oldTimestamp := strconv.FormatInt(testTimestamp-400, 10) // 6 minutes 40 seconds ago headersOld := http.Header{ "Webhook-Signature": []string{"v1,signature"}, "Webhook-Timestamp": []string{oldTimestamp}, "Webhook-Id": []string{testWebhookID}, } // Should fail with default tolerance using time-aware method currentTime := time.Unix(testTimestamp, 0) err := client.Webhooks.VerifySignatureWithToleranceAndTime([]byte(testPayload), headersOld, 5*time.Minute, currentTime) if err == nil { t.Error("VerifySignatureWithToleranceAndTime should have failed with default tolerance") } if err.Error() != "webhook timestamp is too old" { t.Errorf("Expected 'webhook timestamp is too old', got '%s'", err.Error()) } // Should pass timestamp validation with custom tolerance but fail on signature verification err = client.Webhooks.VerifySignatureWithToleranceAndTime([]byte(testPayload), headersOld, 10*time.Minute, currentTime) if err == nil { t.Error("VerifySignatureWithToleranceAndTime should have failed on signature verification") } if err.Error() != "webhook signature verification failed" { t.Errorf("Expected 'webhook signature verification failed', got '%s'", err.Error()) } } func TestWebhookService_UnwrapWithToleranceAndTime(t *testing.T) { client := openai.NewClient( option.WithAPIKey("test-key"), option.WithWebhookSecret(testSecret), ) // Use the time-aware method with the fixed timestamp for testing fixedTime := time.Unix(testTimestamp, 0) webhookEvent, err := client.Webhooks.UnwrapWithToleranceAndTime([]byte(testPayload), createTestHeaders(), 5*time.Minute, fixedTime) if err != nil { t.Errorf("UnwrapWithToleranceAndTime should have succeeded with valid signature: %v", err) } if webhookEvent == nil { t.Error("UnwrapWithToleranceAndTime should return parsed event") } parsed := webhookEvent.AsResponseCompleted() if parsed.ID != "evt_685c059ae3a481909bdc86819b066fb6" { t.Errorf("Expected parsed event ID 'evt_685c059ae3a481909bdc86819b066fb6', got '%s'", parsed.ID) } if parsed.Type != "response.completed" { t.Errorf("Expected parsed event type 'response.completed', got '%s'", parsed.Type) } } func TestWebhookService_VerifySignature_MultipleSignaturesOneValid(t *testing.T) { client := openai.NewClient( option.WithAPIKey("test-key"), option.WithWebhookSecret(testSecret), ) // Test multiple signatures: one invalid, one valid multipleSignatures := "v1,invalid_signature " + testSignature multipleHeaders := http.Header{ "Webhook-Signature": []string{multipleSignatures}, "Webhook-Timestamp": []string{strconv.FormatInt(testTimestamp, 10)}, "Webhook-Id": []string{testWebhookID}, } // Use time-aware method with fixed timestamp fixedTime := time.Unix(testTimestamp, 0) err := client.Webhooks.VerifySignatureWithToleranceAndTime([]byte(testPayload), multipleHeaders, 5*time.Minute, fixedTime) if err != nil { t.Errorf("VerifySignatureWithToleranceAndTime should have succeeded when at least one signature is valid: %v", err) } } func TestWebhookService_VerifySignature_MultipleSignaturesAllInvalid(t *testing.T) { client := openai.NewClient( option.WithAPIKey("test-key"), option.WithWebhookSecret(testSecret), ) // Test multiple invalid signatures multipleInvalidSignatures := "v1,invalid_signature1 v1,invalid_signature2" multipleHeaders := http.Header{ "Webhook-Signature": []string{multipleInvalidSignatures}, "Webhook-Timestamp": []string{strconv.FormatInt(testTimestamp, 10)}, "Webhook-Id": []string{testWebhookID}, } // Use time-aware method with fixed timestamp fixedTime := time.Unix(testTimestamp, 0) err := client.Webhooks.VerifySignatureWithToleranceAndTime([]byte(testPayload), multipleHeaders, 5*time.Minute, fixedTime) if err == nil { t.Error("VerifySignatureWithToleranceAndTime should have failed when all signatures are invalid") } expectedError := "webhook signature verification failed" if err.Error() != expectedError { t.Errorf("Expected error '%s', got '%s'", expectedError, err.Error()) } }