Merge pull request #1172 from trheyi/main

Add GetAllProviders method and related tests for provider information…
This commit is contained in:
Max 2025-09-28 16:01:33 +08:00 committed by GitHub
commit 05ecd1798c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 424 additions and 16 deletions

View file

@ -368,6 +368,18 @@ func (m *Service) GetProvidersByMessageType() map[types.MessageType][]types.Prov
return result
}
// GetAllProviders returns all providers
func (m *Service) GetAllProviders() []types.Provider {
m.mutex.RLock()
defer m.mutex.RUnlock()
providers := make([]types.Provider, 0, len(m.providers))
for _, provider := range m.providers {
providers = append(providers, provider)
}
return providers
}
// GetChannels returns all available channels
func (m *Service) GetChannels() []string {
m.mutex.RLock()

View file

@ -303,6 +303,55 @@ func TestGetProvidersByMessageType(t *testing.T) {
}
}
func TestGetAllProviders(t *testing.T) {
// Setup test environment variables
setupTestEnvironment()
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
defer test.Clean()
err := Load(config.Conf)
require.NoError(t, err, "Load should succeed")
service, ok := Instance.(*Service)
require.True(t, ok, "Instance should be of type *Service")
// Test getting all providers
allProviders := service.GetAllProviders()
t.Logf("Total providers: %d", len(allProviders))
// Should have at least some providers
assert.GreaterOrEqual(t, len(allProviders), 1, "Should have at least one provider")
// Verify each provider has required methods
for _, provider := range allProviders {
assert.NotEmpty(t, provider.GetName(), "Provider should have a name")
assert.NotEmpty(t, provider.GetType(), "Provider should have a type")
// Test GetPublicInfo returns valid data
publicInfo := provider.GetPublicInfo()
assert.NotEmpty(t, publicInfo.Name, "Public info should have name")
assert.NotEmpty(t, publicInfo.Type, "Public info should have type")
assert.NotEmpty(t, publicInfo.Description, "Public info should have description")
assert.NotNil(t, publicInfo.Capabilities, "Public info should have capabilities")
}
// Verify that all providers are accessible
emailProviders := service.GetProviders("email")
// Note: Some providers may support multiple message types, so this is just a sanity check
assert.GreaterOrEqual(t, len(allProviders), len(emailProviders), "Total should be >= email providers")
// Each provider should be unique by name
providerNames := make(map[string]bool)
for _, provider := range allProviders {
providerName := provider.GetName()
assert.False(t, providerNames[providerName], "Provider names should be unique: %s", providerName)
providerNames[providerName] = true
}
}
func TestValidateMessage(t *testing.T) {
// Setup test environment variables
setupTestEnvironment()

View file

@ -209,6 +209,27 @@ func (p *Provider) GetName() string {
return p.config.Name
}
// GetPublicInfo returns public information about the provider
func (p *Provider) GetPublicInfo() types.ProviderPublicInfo {
description := "SMTP email provider"
if p.config.Description != "" {
description = p.config.Description
}
return types.ProviderPublicInfo{
Name: p.config.Name,
Type: "mailer",
Description: description,
Capabilities: []string{"email"},
Features: types.Features{
SupportsWebhooks: false,
SupportsReceiving: p.SupportsReceiving(),
SupportsTracking: false,
SupportsScheduling: false,
},
}
}
// Validate validates the provider configuration
func (p *Provider) Validate() error {
if p.host == "" {

View file

@ -685,3 +685,86 @@ func BenchmarkBuildMessage(b *testing.B) {
}
}
}
func TestProvider_GetPublicInfo(t *testing.T) {
config := types.ProviderConfig{
Name: "test-mailer",
Connector: "mailer",
Description: "Test SMTP Provider",
Options: map[string]interface{}{
"smtp": map[string]interface{}{
"host": "smtp.example.com",
"port": 587,
"username": "test@example.com",
"password": "testpass",
"from": "test@example.com",
},
},
}
provider, err := NewMailerProvider(config)
require.NoError(t, err)
info := provider.GetPublicInfo()
// Verify public information
assert.Equal(t, "test-mailer", info.Name)
assert.Equal(t, "mailer", info.Type)
assert.Equal(t, "Test SMTP Provider", info.Description)
assert.Equal(t, false, info.Features.SupportsWebhooks)
assert.Equal(t, false, info.Features.SupportsReceiving) // No IMAP config
assert.Equal(t, false, info.Features.SupportsTracking)
assert.Equal(t, false, info.Features.SupportsScheduling)
// Verify capabilities
assert.Contains(t, info.Capabilities, "email")
}
func TestProvider_GetPublicInfo_DefaultDescription(t *testing.T) {
config := types.ProviderConfig{
Name: "test-mailer-no-desc",
Connector: "mailer",
Options: map[string]interface{}{
"smtp": map[string]interface{}{
"host": "smtp.example.com",
"port": 587,
"username": "test@example.com",
"password": "testpass",
"from": "test@example.com",
},
},
}
provider, err := NewMailerProvider(config)
require.NoError(t, err)
info := provider.GetPublicInfo()
// Should use default description when none provided
assert.Equal(t, "SMTP email provider", info.Description)
}
func TestProvider_TriggerWebhook(t *testing.T) {
config := types.ProviderConfig{
Name: "test-mailer",
Connector: "mailer",
Options: map[string]interface{}{
"smtp": map[string]interface{}{
"host": "smtp.example.com",
"port": 587,
"username": "test@example.com",
"password": "testpass",
"from": "test@example.com",
},
},
}
provider, err := NewMailerProvider(config)
require.NoError(t, err)
// TriggerWebhook should return an error for SMTP providers
msg, err := provider.TriggerWebhook(nil)
assert.Error(t, err)
assert.Nil(t, msg)
assert.Contains(t, err.Error(), "TriggerWebhook not supported for SMTP/mailer provider")
}

View file

@ -96,6 +96,27 @@ func (p *Provider) GetName() string {
return p.config.Name
}
// GetPublicInfo returns public information about the provider
func (p *Provider) GetPublicInfo() types.ProviderPublicInfo {
description := "Mailgun email service provider"
if p.config.Description != "" {
description = p.config.Description
}
return types.ProviderPublicInfo{
Name: p.config.Name,
Type: "mailgun",
Description: description,
Capabilities: []string{"email", "webhooks", "tracking"},
Features: types.Features{
SupportsWebhooks: true,
SupportsReceiving: false,
SupportsTracking: true,
SupportsScheduling: true,
},
}
}
// Validate validates the provider configuration
func (p *Provider) Validate() error {
if p.domain == "" {

View file

@ -169,3 +169,55 @@ func TestProvider_TriggerWebhook_InvalidInput(t *testing.T) {
assert.Nil(t, msg)
assert.Contains(t, err.Error(), "expected *gin.Context")
}
func TestProvider_GetPublicInfo(t *testing.T) {
config := types.ProviderConfig{
Name: "test-mailgun",
Connector: "mailgun",
Description: "Test Mailgun Provider",
Options: map[string]interface{}{
"domain": "test.mailgun.org",
"api_key": "test-api-key",
"from": "test@test.mailgun.org",
},
}
provider, err := NewMailgunProvider(config)
require.NoError(t, err)
info := provider.GetPublicInfo()
// Verify public information
assert.Equal(t, "test-mailgun", info.Name)
assert.Equal(t, "mailgun", info.Type)
assert.Equal(t, "Test Mailgun Provider", info.Description)
assert.Equal(t, true, info.Features.SupportsWebhooks)
assert.Equal(t, true, info.Features.SupportsTracking)
assert.Equal(t, true, info.Features.SupportsScheduling)
assert.Equal(t, false, info.Features.SupportsReceiving)
// Verify capabilities
assert.Contains(t, info.Capabilities, "email")
assert.Contains(t, info.Capabilities, "webhooks")
assert.Contains(t, info.Capabilities, "tracking")
}
func TestProvider_GetPublicInfo_DefaultDescription(t *testing.T) {
config := types.ProviderConfig{
Name: "test-mailgun-no-desc",
Connector: "mailgun",
Options: map[string]interface{}{
"domain": "test.mailgun.org",
"api_key": "test-api-key",
"from": "test@test.mailgun.org",
},
}
provider, err := NewMailgunProvider(config)
require.NoError(t, err)
info := provider.GetPublicInfo()
// Should use default description when none provided
assert.Equal(t, "Mailgun email service provider", info.Description)
}

View file

@ -128,6 +128,41 @@ func (p *Provider) GetName() string {
return p.config.Name
}
// GetPublicInfo returns public information about the provider
func (p *Provider) GetPublicInfo() types.ProviderPublicInfo {
description := "Twilio multi-channel communication provider"
if p.config.Description != "" {
description = p.config.Description
}
capabilities := []string{}
if p.fromPhone != "" || p.messagingServiceSID != "" {
capabilities = append(capabilities, "sms")
}
if p.fromPhone != "" {
capabilities = append(capabilities, "whatsapp")
}
if p.sendGridAPIKey != "" {
capabilities = append(capabilities, "email")
}
if len(capabilities) == 0 {
capabilities = []string{"sms", "whatsapp", "email"} // Default capabilities
}
return types.ProviderPublicInfo{
Name: p.config.Name,
Type: "twilio",
Description: description,
Capabilities: capabilities,
Features: types.Features{
SupportsWebhooks: true,
SupportsReceiving: false,
SupportsTracking: true,
SupportsScheduling: true,
},
}
}
// Validate validates the provider configuration
func (p *Provider) Validate() error {
if p.accountSID == "" {

View file

@ -22,6 +22,9 @@ type Provider interface {
// GetName returns the provider name/identifier
GetName() string
// GetPublicInfo returns public information about the provider (name, description, type)
GetPublicInfo() ProviderPublicInfo
// Validate validates the provider configuration
Validate() error
@ -46,6 +49,9 @@ type Messenger interface {
// GetProviders returns all providers for a channel type
GetProviders(channelType string) []Provider
// GetAllProviders returns all providers
GetAllProviders() []Provider
// GetChannels returns all available channels
GetChannels() []string

View file

@ -117,3 +117,20 @@ type SendResult struct {
SentAt time.Time `json:"sent_at"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// ProviderPublicInfo defines the public information structure for providers
type ProviderPublicInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
Capabilities []string `json:"capabilities"`
Features Features `json:"features"`
}
// Features defines the features supported by a provider
type Features struct {
SupportsWebhooks bool `json:"supports_webhooks"`
SupportsReceiving bool `json:"supports_receiving"`
SupportsTracking bool `json:"supports_tracking"`
SupportsScheduling bool `json:"supports_scheduling"`
}

View file

@ -1,20 +1,25 @@
package messenger
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/messenger"
"github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/messenger/types"
oauthTypes "github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/openapi/response"
)
// Attach attaches the messenger webhook handlers to the router
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
func Attach(group *gin.RouterGroup, oauth oauthTypes.OAuth) {
// Webhook endpoint with provider parameter - public interface
group.GET("/webhook/:provider", webhookHandler)
group.POST("/webhook/:provider", webhookHandler)
// Private API endpoints for provider and channel information
group.GET("/providers", oauth.Guard, getProvidersHandler)
group.GET("/providers/:name", oauth.Guard, getProviderHandler)
group.GET("/channels", oauth.Guard, getChannelsHandler)
}
// webhookHandler is the handler for webhook endpoint
@ -22,18 +27,22 @@ func webhookHandler(c *gin.Context) {
// Get provider name from URL parameter
providerName := c.Param("provider")
if providerName == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "provider parameter is required",
})
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Provider parameter is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Check if messenger service is available
if messenger.Instance == nil {
log.Warn("[OpenAPI Messenger] Messenger service not initialized")
c.JSON(http.StatusServiceUnavailable, gin.H{
"error": "messenger service not available",
})
errorResp := &response.ErrorResponse{
Code: response.ErrTemporarilyUnavailable.Code,
ErrorDescription: "Messenger service not available",
}
response.RespondWithError(c, response.StatusServiceUnavailable, errorResp)
return
}
@ -41,17 +50,120 @@ func webhookHandler(c *gin.Context) {
err := messenger.Instance.TriggerWebhook(providerName, c)
if err != nil {
log.Error("[OpenAPI Messenger] Failed to process webhook for provider %s: %v", providerName, err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed to process webhook",
"details": err.Error(),
})
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to process webhook: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Return success response
c.JSON(http.StatusOK, gin.H{
successResp := gin.H{
"status": "received",
"message": "webhook processed successfully",
"provider": providerName,
})
}
response.RespondWithSuccess(c, response.StatusOK, successResp)
}
// getProviderHandler returns public information about a specific provider
func getProviderHandler(c *gin.Context) {
providerName := c.Param("name")
if providerName == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Provider name is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Check if messenger service is available
if messenger.Instance == nil {
errorResp := &response.ErrorResponse{
Code: response.ErrTemporarilyUnavailable.Code,
ErrorDescription: "Messenger service not available",
}
response.RespondWithError(c, response.StatusServiceUnavailable, errorResp)
return
}
// Get provider
provider, err := messenger.Instance.GetProvider(providerName)
if err != nil {
errorResp := &response.ErrorResponse{
Code: "provider_not_found",
ErrorDescription: "Provider not found: " + providerName,
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
// Return public information directly
response.RespondWithSuccess(c, response.StatusOK, provider.GetPublicInfo())
}
// getProvidersHandler returns public information about all providers, with optional channel type filter
func getProvidersHandler(c *gin.Context) {
// Check if messenger service is available
if messenger.Instance == nil {
errorResp := &response.ErrorResponse{
Code: response.ErrTemporarilyUnavailable.Code,
ErrorDescription: "Messenger service not available",
}
response.RespondWithError(c, response.StatusServiceUnavailable, errorResp)
return
}
// Get optional channel type filter from query parameter
channelType := c.Query("channel_type")
var providers []types.Provider
if channelType != "" {
// Filter by channel type
providers = messenger.Instance.GetProviders(channelType)
} else {
// Get all providers
providers = messenger.Instance.GetAllProviders()
}
// Convert to public information
publicProviders := make([]interface{}, 0, len(providers))
for _, provider := range providers {
publicProviders = append(publicProviders, provider.GetPublicInfo())
}
successResp := gin.H{
"providers": publicProviders,
"count": len(publicProviders),
}
if channelType != "" {
successResp["channel_type"] = channelType
}
response.RespondWithSuccess(c, response.StatusOK, successResp)
}
// getChannelsHandler returns all available channels
func getChannelsHandler(c *gin.Context) {
// Check if messenger service is available
if messenger.Instance == nil {
errorResp := &response.ErrorResponse{
Code: response.ErrTemporarilyUnavailable.Code,
ErrorDescription: "Messenger service not available",
}
response.RespondWithError(c, response.StatusServiceUnavailable, errorResp)
return
}
// Get all channels
channels := messenger.Instance.GetChannels()
successResp := gin.H{
"channels": channels,
"count": len(channels),
}
response.RespondWithSuccess(c, response.StatusOK, successResp)
}