mirror of
				https://github.com/owncast/owncast.git
				synced 2025-10-31 10:08:10 +08:00 
			
		
		
		
	 740dd9c6fa
			
		
	
	740dd9c6fa
	
	
	
		
			
			* Initial plan * Add server status as default field in all webhooks Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Fix goimports linter error by removing trailing whitespace Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Move serverURL from status object to separate webhook field per feedback Per code review feedback, serverURL is a configuration value, not a status property. This change: - Removes ServerURL from models.Status struct - Adds ServerURL as separate field in WebhookEvent - Populates ServerURL directly when sending webhooks using configrepository.GetServerURL() - Updates all tests to expect new structure This provides the same functionality (server URL in all webhooks) while correctly treating it as configuration rather than status. Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Add omitempty tag to ServerURL field in WebhookEvent struct Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Fix webhook duplication by moving status to eventData for all events Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Restore type safety to webhook EventData using proper typed structs Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Move ServerURL from top-level WebhookEvent to eventData for all webhook types Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Update core/webhooks/webhooks.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Create BaseWebhookData struct for common webhook fields using struct embedding Co-authored-by: gabek <414923+gabek@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gabek <414923+gabek@users.noreply.github.com> Co-authored-by: Gabe Kangas <gabek@real-ity.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
		
			
				
	
	
		
			315 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			315 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package webhooks
 | |
| 
 | |
| import (
 | |
| 	"encoding/json"
 | |
| 	"net/http"
 | |
| 	"net/http/httptest"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/owncast/owncast/core/chat/events"
 | |
| 	"github.com/owncast/owncast/models"
 | |
| 	"github.com/owncast/owncast/persistence/configrepository"
 | |
| 	"github.com/owncast/owncast/persistence/webhookrepository"
 | |
| )
 | |
| 
 | |
| func TestSendChatEvent(t *testing.T) {
 | |
| 	timestamp := time.Unix(72, 6).UTC()
 | |
| 	user := models.User{
 | |
| 		ID:              "user id",
 | |
| 		DisplayName:     "display name",
 | |
| 		DisplayColor:    4,
 | |
| 		CreatedAt:       time.Unix(3, 26).UTC(),
 | |
| 		DisabledAt:      nil,
 | |
| 		PreviousNames:   []string{"somebody"},
 | |
| 		NameChangedAt:   nil,
 | |
| 		Scopes:          []string{},
 | |
| 		IsBot:           false,
 | |
| 		AuthenticatedAt: nil,
 | |
| 		Authenticated:   false,
 | |
| 	}
 | |
| 
 | |
| 	checkPayload(t, models.MessageSent, func() {
 | |
| 		SendChatEvent(&events.UserMessageEvent{
 | |
| 			Event: events.Event{
 | |
| 				Type:      events.MessageSent,
 | |
| 				ID:        "id",
 | |
| 				Timestamp: timestamp,
 | |
| 			},
 | |
| 			UserEvent: events.UserEvent{
 | |
| 				User:     &user,
 | |
| 				ClientID: 51,
 | |
| 				HiddenAt: nil,
 | |
| 			},
 | |
| 			MessageEvent: events.MessageEvent{
 | |
| 				OutboundEvent: nil,
 | |
| 				Body:          "body",
 | |
| 				RawBody:       "raw body",
 | |
| 			},
 | |
| 		})
 | |
| 	}, `{
 | |
| 		"body": "body",
 | |
| 		"clientId": 51,
 | |
| 		"id": "id",
 | |
| 		"rawBody": "raw body",
 | |
| 		"serverURL": "http://localhost:8080",
 | |
| 		"status": {
 | |
| 			"lastConnectTime": null,
 | |
| 			"lastDisconnectTime": null,
 | |
| 			"online": true,
 | |
| 			"overallMaxViewerCount": 420,
 | |
| 			"sessionMaxViewerCount": 69,
 | |
| 			"streamTitle": "my stream",
 | |
| 			"versionNumber": "1.2.3",
 | |
| 			"viewerCount": 5
 | |
| 		},
 | |
| 		"timestamp": "1970-01-01T00:01:12.000000006Z",
 | |
| 		"user": {
 | |
| 			"authenticated": false,
 | |
| 			"createdAt": "1970-01-01T00:00:03.000000026Z",
 | |
| 			"displayColor": 4,
 | |
| 			"displayName": "display name",
 | |
| 			"id": "user id",
 | |
| 			"isBot": false,
 | |
| 			"previousNames": ["somebody"]
 | |
| 		},
 | |
| 		"visible": true
 | |
| 	}`)
 | |
| }
 | |
| 
 | |
| func TestSendChatEventUsernameChanged(t *testing.T) {
 | |
| 	timestamp := time.Unix(72, 6).UTC()
 | |
| 	user := models.User{
 | |
| 		ID:              "user id",
 | |
| 		DisplayName:     "display name",
 | |
| 		DisplayColor:    4,
 | |
| 		CreatedAt:       time.Unix(3, 26).UTC(),
 | |
| 		DisabledAt:      nil,
 | |
| 		PreviousNames:   []string{"somebody"},
 | |
| 		NameChangedAt:   nil,
 | |
| 		Scopes:          []string{},
 | |
| 		IsBot:           false,
 | |
| 		AuthenticatedAt: nil,
 | |
| 		Authenticated:   false,
 | |
| 	}
 | |
| 
 | |
| 	checkPayload(t, models.UserNameChanged, func() {
 | |
| 		SendChatEventUsernameChanged(events.NameChangeEvent{
 | |
| 			Event: events.Event{
 | |
| 				Type:      events.UserNameChanged,
 | |
| 				ID:        "id",
 | |
| 				Timestamp: timestamp,
 | |
| 			},
 | |
| 			UserEvent: events.UserEvent{
 | |
| 				User:     &user,
 | |
| 				ClientID: 51,
 | |
| 				HiddenAt: nil,
 | |
| 			},
 | |
| 			NewName: "new name",
 | |
| 		})
 | |
| 	}, `{
 | |
| 		"id": "id",
 | |
| 		"newName": "new name",
 | |
| 		"serverURL": "http://localhost:8080",
 | |
| 		"status": {
 | |
| 			"lastConnectTime": null,
 | |
| 			"lastDisconnectTime": null,
 | |
| 			"online": true,
 | |
| 			"overallMaxViewerCount": 420,
 | |
| 			"sessionMaxViewerCount": 69,
 | |
| 			"streamTitle": "my stream",
 | |
| 			"versionNumber": "1.2.3",
 | |
| 			"viewerCount": 5
 | |
| 		},
 | |
| 		"timestamp": "1970-01-01T00:01:12.000000006Z",
 | |
| 		"user": {
 | |
| 			"authenticated": false,
 | |
| 			"createdAt": "1970-01-01T00:00:03.000000026Z",
 | |
| 			"displayColor": 4,
 | |
| 			"displayName": "display name",
 | |
| 			"id": "user id",
 | |
| 			"isBot": false,
 | |
| 			"previousNames": ["somebody"]
 | |
| 		}
 | |
| 	}`)
 | |
| }
 | |
| 
 | |
| func TestSendChatEventUserJoined(t *testing.T) {
 | |
| 	timestamp := time.Unix(72, 6).UTC()
 | |
| 	user := models.User{
 | |
| 		ID:              "user id",
 | |
| 		DisplayName:     "display name",
 | |
| 		DisplayColor:    4,
 | |
| 		CreatedAt:       time.Unix(3, 26).UTC(),
 | |
| 		DisabledAt:      nil,
 | |
| 		PreviousNames:   []string{"somebody"},
 | |
| 		NameChangedAt:   nil,
 | |
| 		Scopes:          []string{},
 | |
| 		IsBot:           false,
 | |
| 		AuthenticatedAt: nil,
 | |
| 		Authenticated:   false,
 | |
| 	}
 | |
| 
 | |
| 	checkPayload(t, models.UserJoined, func() {
 | |
| 		SendChatEventUserJoined(events.UserJoinedEvent{
 | |
| 			Event: events.Event{
 | |
| 				Type:      events.UserJoined,
 | |
| 				ID:        "id",
 | |
| 				Timestamp: timestamp,
 | |
| 			},
 | |
| 			UserEvent: events.UserEvent{
 | |
| 				User:     &user,
 | |
| 				ClientID: 51,
 | |
| 				HiddenAt: nil,
 | |
| 			},
 | |
| 		})
 | |
| 	}, `{
 | |
| 		"id": "id",
 | |
| 		"serverURL": "http://localhost:8080",
 | |
| 		"status": {
 | |
| 			"lastConnectTime": null,
 | |
| 			"lastDisconnectTime": null,
 | |
| 			"online": true,
 | |
| 			"overallMaxViewerCount": 420,
 | |
| 			"sessionMaxViewerCount": 69,
 | |
| 			"streamTitle": "my stream",
 | |
| 			"versionNumber": "1.2.3",
 | |
| 			"viewerCount": 5
 | |
| 		},
 | |
| 		"timestamp": "1970-01-01T00:01:12.000000006Z",
 | |
| 		"user": {
 | |
| 			"authenticated": false,
 | |
| 			"createdAt": "1970-01-01T00:00:03.000000026Z",
 | |
| 			"displayColor": 4,
 | |
| 			"displayName": "display name",
 | |
| 			"id": "user id",
 | |
| 			"isBot": false,
 | |
| 			"previousNames": ["somebody"]
 | |
| 		}
 | |
| 	}`)
 | |
| }
 | |
| 
 | |
| func TestSendChatEventSetMessageVisibility(t *testing.T) {
 | |
| 	timestamp := time.Unix(72, 6).UTC()
 | |
| 
 | |
| 	checkPayload(t, models.VisibiltyToggled, func() {
 | |
| 		SendChatEventSetMessageVisibility(events.SetMessageVisibilityEvent{
 | |
| 			Event: events.Event{
 | |
| 				Type:      events.VisibiltyUpdate,
 | |
| 				ID:        "id",
 | |
| 				Timestamp: timestamp,
 | |
| 			},
 | |
| 			UserMessageEvent: events.UserMessageEvent{},
 | |
| 			MessageIDs:       []string{"message1", "message2"},
 | |
| 			Visible:          false,
 | |
| 		})
 | |
| 	}, `{
 | |
| 		"id": "id",
 | |
| 		"ids": [
 | |
| 			"message1",
 | |
| 			"message2"
 | |
| 		],
 | |
| 		"serverURL": "http://localhost:8080",
 | |
| 		"status": {
 | |
| 			"lastConnectTime": null,
 | |
| 			"lastDisconnectTime": null,
 | |
| 			"online": true,
 | |
| 			"overallMaxViewerCount": 420,
 | |
| 			"sessionMaxViewerCount": 69,
 | |
| 			"streamTitle": "my stream",
 | |
| 			"versionNumber": "1.2.3",
 | |
| 			"viewerCount": 5
 | |
| 		},
 | |
| 		"timestamp": "1970-01-01T00:01:12.000000006Z",
 | |
| 		"user": null,
 | |
| 		"visible": false
 | |
| 	}`)
 | |
| }
 | |
| 
 | |
| // TestWebhookHasServerStatus verifies that all webhook events include server status
 | |
| func TestWebhookHasServerStatus(t *testing.T) {
 | |
| 	// Set up server configuration
 | |
| 	configRepo := configrepository.Get()
 | |
| 	configRepo.SetServerURL("http://localhost:8080")
 | |
| 
 | |
| 	eventChannel := make(chan WebhookEvent)
 | |
| 
 | |
| 	// Set up a server.
 | |
| 	svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 		data := WebhookEvent{}
 | |
| 		json.NewDecoder(r.Body).Decode(&data)
 | |
| 		eventChannel <- data
 | |
| 	}))
 | |
| 	defer svr.Close()
 | |
| 
 | |
| 	webhooksRepo := webhookrepository.Get()
 | |
| 
 | |
| 	// Subscribe to the webhook.
 | |
| 	hook, err := webhooksRepo.InsertWebhook(svr.URL, []models.EventType{models.UserJoined})
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	defer func() {
 | |
| 		if err := webhooksRepo.DeleteWebhook(hook); err != nil {
 | |
| 			t.Error(err)
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	// Send a chat event
 | |
| 	timestamp := time.Unix(72, 6).UTC()
 | |
| 	user := models.User{
 | |
| 		ID:              "user id",
 | |
| 		DisplayName:     "display name",
 | |
| 		DisplayColor:    4,
 | |
| 		CreatedAt:       time.Unix(3, 26).UTC(),
 | |
| 		DisabledAt:      nil,
 | |
| 		PreviousNames:   []string{"somebody"},
 | |
| 		NameChangedAt:   nil,
 | |
| 		Scopes:          []string{},
 | |
| 		IsBot:           false,
 | |
| 		AuthenticatedAt: nil,
 | |
| 		Authenticated:   false,
 | |
| 	}
 | |
| 
 | |
| 	SendChatEventUserJoined(events.UserJoinedEvent{
 | |
| 		Event: events.Event{
 | |
| 			Type:      events.UserJoined,
 | |
| 			ID:        "id",
 | |
| 			Timestamp: timestamp,
 | |
| 		},
 | |
| 		UserEvent: events.UserEvent{
 | |
| 			User:     &user,
 | |
| 			ClientID: 51,
 | |
| 			HiddenAt: nil,
 | |
| 		},
 | |
| 	})
 | |
| 
 | |
| 	// Capture the event
 | |
| 	event := <-eventChannel
 | |
| 
 | |
| 	// Verify the webhook event has a status field in eventData
 | |
| 	eventData, ok := event.EventData.(map[string]interface{})
 | |
| 	if !ok {
 | |
| 		t.Error("Expected EventData to be a map")
 | |
| 	}
 | |
| 
 | |
| 	status, ok := eventData["status"].(map[string]interface{})
 | |
| 	if !ok {
 | |
| 		t.Error("Expected eventData to contain status field")
 | |
| 	}
 | |
| 
 | |
| 	versionNumber, ok := status["versionNumber"].(string)
 | |
| 	if !ok || versionNumber == "" {
 | |
| 		t.Error("Expected eventData.status to have versionNumber, but it was empty")
 | |
| 	}
 | |
| 
 | |
| 	serverURL, ok := eventData["serverURL"].(string)
 | |
| 	if !ok || serverURL == "" {
 | |
| 		t.Error("Expected eventData to have serverURL, but it was empty")
 | |
| 	}
 | |
| 
 | |
| 	if event.Type != models.UserJoined {
 | |
| 		t.Errorf("Expected event type %v but got %v", models.UserJoined, event.Type)
 | |
| 	}
 | |
| }
 |