Repository: nicklaw5/helix
Pull request #156 (2022-11-25 09:44:51)

feat: charity updates
- add new charity eventsubs
- add new charity donations endpoint
- add unit test for charity donations


Patch:
diff --git a/charity.go b/charity.go
index 8808896..0eec972 100644
--- a/charity.go
+++ b/charity.go
@@ -1,5 +1,29 @@
 package helix
 
+type CharityDonationData struct {
+	CampaignID string                `json:"campaign_id"`
+	UserID     string                `json:"user_id"`
+	UserName   string                `json:"user_name"`
+	UserLogin  string                `json:"user_login"`
+	Amount     CharityCampaignAmount `json:"amount"`
+}
+
+type ManyCharityDonations struct {
+	Donations  []CharityDonationData `json:"data"`
+	Pagination Pagination            `json:"pagination"`
+}
+
+type CharityDonationParams struct {
+	BroadcasterID string `query:"broadcaster_id"`
+	After         string `query:"after"`
+	First         int    `query:"first,20"` // Limit 100
+}
+
+type CharityDonationsResponse struct {
+	ResponseCommon
+	Data ManyCharityDonations
+}
+
 type CharityCampaignAmount struct {
 	Value         int64  `json:"amount"`
 	DecimalPlaces int64  `json:"decimal_places"`
@@ -49,3 +73,18 @@ func (c *Client) GetCharityCampaigns(params *CharityCampaignsParams) (*CharityCa
 
 	return events, nil
 }
+
+// Required scope: channel:read:charity
+func (c *Client) GetCharityDonations(params *CharityDonationParams) (*CharityDonationsResponse, error) {
+	resp, err := c.get("/charity/donations", &ManyCharityDonations{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	events := &CharityDonationsResponse{}
+	resp.HydrateResponseCommon(&events.ResponseCommon)
+	events.Data.Donations = resp.Data.(*ManyCharityDonations).Donations
+	events.Data.Pagination = resp.Data.(*ManyCharityDonations).Pagination
+
+	return events, nil
+}
diff --git a/eventsub.go b/eventsub.go
index deb5514..c23c74f 100644
--- a/eventsub.go
+++ b/eventsub.go
@@ -116,6 +116,9 @@ const (
 	EventSubTypeHypeTrainProgress                         = "channel.hype_train.progress"
 	EventSubTypeHypeTrainEnd                              = "channel.hype_train.end"
 	EventSubTypeCharityDonation                           = "channel.charity_campaign.donate"
+	EventSubTypeCharityProgress                           = "channel.charity_campaign.progress"
+	EventSubTypeCharityStop                               = "channel.charity_campaign.stop"
+	EventSubTypeCharityStart                              = "channel.charity_campaign.start"
 	EventSubTypeStreamOnline                              = "stream.online"
 	EventSubTypeStreamOffline                             = "stream.offline"
 	EventSubTypeUserAuthorizationRevoke                   = "user.authorization.revoke"
@@ -577,6 +580,8 @@ type EventSubCharityAmount struct {
 
 type EventSubCharityDonationEvent struct {
 	CharityCampaignID    string                `json:"campaign_id"`
+	CharityDescription   string                `json:"campaign_description"`
+	CharityWebsite       string                `json:"campaign_website"`
 	CharityName          string                `json:"charity_name"`
 	CharityLogoURL       string                `json:"charity_logo"`
 	BroadcasterUserID    string                `json:"broadcaster_user_id"`
@@ -588,6 +593,55 @@ type EventSubCharityDonationEvent struct {
 	Amount               EventSubCharityAmount `json:"amount"`
 }
 
+type EventSubCharityProgressEvent struct {
+	CharityCampaignID    string                `json:"campaign_id"`
+	CharityDescription   string                `json:"campaign_description"`
+	CharityWebsite       string                `json:"campaign_website"`
+	CharityName          string                `json:"charity_name"`
+	CharityLogoURL       string                `json:"charity_logo"`
+	BroadcasterUserID    string                `json:"broadcaster_id"`
+	BroadcasterUserName  string                `json:"broadcaster_name"`
+	BroadcasterUserLogin string                `json:"broadcaster_user_login"`
+	UserID               string                `json:"user_id"`
+	UserName             string                `json:"user_name"`
+	UserLogin            string                `json:"user_login"`
+	Amount               EventSubCharityAmount `json:"amount"`
+}
+
+type EventSubCharityStopEvent struct {
+	CharityCampaignID    string                `json:"campaign_id"`
+	CharityDescription   string                `json:"campaign_description"`
+	CharityWebsite       string                `json:"campaign_website"`
+	CharityName          string                `json:"charity_name"`
+	CharityLogoURL       string                `json:"charity_logo"`
+	BroadcasterUserID    string                `json:"broadcaster_id"`
+	BroadcasterUserName  string                `json:"broadcaster_name"`
+	BroadcasterUserLogin string                `json:"broadcaster_login"`
+	UserID               string                `json:"user_id"`
+	UserName             string                `json:"user_name"`
+	UserLogin            string                `json:"user_login"`
+	CurrentAmount        EventSubCharityAmount `json:"current_amount"`
+	TargetAmount         EventSubCharityAmount `json:"target_amount"`
+	StoppedAt            Time                  `json:"stopped_at"`
+}
+
+type EventSubCharityStartEvent struct {
+	CharityCampaignID    string                `json:"campaign_id"`
+	CharityDescription   string                `json:"campaign_description"`
+	CharityWebsite       string                `json:"campaign_website"`
+	CharityName          string                `json:"charity_name"`
+	CharityLogoURL       string                `json:"charity_logo"`
+	BroadcasterUserID    string                `json:"broadcaster_id"`
+	BroadcasterUserName  string                `json:"broadcaster_name"`
+	BroadcasterUserLogin string                `json:"broadcaster_login"`
+	UserID               string                `json:"user_id"`
+	UserName             string                `json:"user_name"`
+	UserLogin            string                `json:"user_login"`
+	CurrentAmount        EventSubCharityAmount `json:"current_amount"`
+	TargetAmount         EventSubCharityAmount `json:"target_amount"`
+	StartedAt            Time                  `json:"started_at"`
+}
+
 // Get all EventSub Subscriptions
 func (c *Client) GetEventSubSubscriptions(params *EventSubSubscriptionsParams) (*EventSubSubscriptionsResponse, error) {
 	resp, err := c.get("/eventsub/subscriptions", &ManyEventSubSubscriptions{}, params)


Test patch:
diff --git a/charity_test.go b/charity_test.go
index 1a25201..fba3d7d 100644
--- a/charity_test.go
+++ b/charity_test.go
@@ -84,3 +84,83 @@ func TestGetCharityCampaigns(t *testing.T) {
 		t.Error("expected error does match return error")
 	}
 }
+
+func TestGetCharityDonations(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode     int
+		options        *Options
+		DonationParams *CharityDonationParams
+		respBody       string
+	}{
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&CharityDonationParams{BroadcasterID: ""},
+			`{"error":"Bad Request","status":400,"message":"Missing required parameter \"broadcaster_id\""}`,
+		},
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&CharityDonationParams{BroadcasterID: "123456"},
+			`{ "data": [{ "campaign_id": "100249558", "user_id": "123456", "user_name": "SunnySideUp", "user_login": "sunnysideup", "amount": { "value": 86000, "decimal_places": 2, "currency": "USD" }}] }`},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.GetCharityDonations(testCase.DonationParams)
+		if err != nil {
+			t.Error(err)
+		}
+
+		if resp.StatusCode != testCase.statusCode {
+			t.Errorf("expected status code to be %d, got %d", testCase.statusCode, resp.StatusCode)
+		}
+
+		if resp.StatusCode == http.StatusBadRequest {
+			if resp.Error != "Bad Request" {
+				t.Errorf("expected error to be %s, got %s", "Bad Request", resp.Error)
+			}
+
+			if resp.ErrorStatus != http.StatusBadRequest {
+				t.Errorf("expected error status to be %d, got %d", http.StatusBadRequest, resp.ErrorStatus)
+			}
+
+			expectedErrMsg := "Missing required parameter \"broadcaster_id\""
+			if resp.ErrorMessage != expectedErrMsg {
+				t.Errorf("expected error message to be %s, got %s", expectedErrMsg, resp.ErrorMessage)
+			}
+
+			continue
+		}
+
+		if len(resp.Data.Donations) != 1 {
+			t.Errorf("expected charity campaigns len to be 1, got %d", len(resp.Data.Donations))
+		}
+		if resp.Data.Donations[0].CampaignID != "100249558" {
+			t.Errorf("invalid charity name %q, expected 100249588", resp.Data.Donations[0].CampaignID)
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(400, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+	}
+
+	_, err := c.GetCharityDonations(&CharityDonationParams{})
+	if err == nil {
+		t.Error("expected error but got nil")
+	}
+
+	if err.Error() != "Failed to execute API request: Oops, that's bad :(" {
+		t.Error("expected error does match return error")
+	}
+}