Repository: nicklaw5/helix
Pull request #183 (2023-02-20 17:17:08)

Raid support
Added support for starting and canceling raids. 
https://dev.twitch.tv/docs/api/reference/#start-a-raid
https://dev.twitch.tv/docs/api/reference/#cancel-a-raid

(First real PR, let me know if I missed anything 😅)


Patch:
diff --git a/SUPPORTED_ENDPOINTS.md b/SUPPORTED_ENDPOINTS.md
index 37c0ab5..9fdfa40 100644
--- a/SUPPORTED_ENDPOINTS.md
+++ b/SUPPORTED_ENDPOINTS.md
@@ -94,8 +94,8 @@
 - [x] Get Predictions
 - [x] Create Prediction
 - [x] End Prediction
-- [ ] Start a raid
-- [ ] Cancel a raid
+- [x] Start a raid
+- [x] Cancel a raid
 - [ ] Get Channel Stream Schedule
 - [ ] Get Channel iCalendar
 - [ ] Update Channel Stream Schedule
diff --git a/docs/raid_docs.md b/docs/raid_docs.md
new file mode 100644
index 0000000..0de907c
--- /dev/null
+++ b/docs/raid_docs.md
@@ -0,0 +1,46 @@
+# Raid Documentation
+
+## Start Raid
+
+This is an example of how to start a raid.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.StartRaid(&helix.StartRaidParams{
+    FromBroadcasterID: "22484632",
+  	ToBroadcasterID: "71092938" 
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## Cancel Raid
+
+This is an example of how to cancel a raid.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := resp, err := client.CancelRaid(&helix.CancelRaidParams{
+    BroadcasterID: "22484632", 
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
\ No newline at end of file
diff --git a/raids.go b/raids.go
new file mode 100644
index 0000000..aa3634f
--- /dev/null
+++ b/raids.go
@@ -0,0 +1,66 @@
+package helix
+
+// RaidResponse is the response from StartRaid
+type RaidResponse struct {
+	ResponseCommon
+	Data StartRaidResponse
+}
+
+// StartRaidParams are the parameters for StartRaid
+type StartRaidParams struct {
+	FromBroadcasterID string `query:"from_broadcaster_id"`
+	ToBroadcasterID   string `query:"to_broadcaster_id"`
+}
+
+// StartRaidResponse is the response data in RaidResponse
+type StartRaidResponse struct {
+	Data []RaidDetails `json:"data"`
+}
+
+// RaidDetails describes details of the ongoing raid
+type RaidDetails struct {
+	CreatedAt Time `json:"created_at"`
+	IsMature  bool `json:"is_mature"`
+}
+
+// StartRaid raids another channel by sending the broadcaster’s viewers to the targeted channel.
+// When called, the Twitch UX pops up a window at the top of the chat room that identifies the number of viewers in the raid.
+// The raid occurs when the broadcaster clicks Raid Now or after the 90-second countdown expires.
+// Required scope: channel:manage:raids
+// Rate limit: 10 requests within a 10-minute window.
+func (c *Client) StartRaid(params *StartRaidParams) (*RaidResponse, error) {
+	resp, err := c.post("/raids", &StartRaidResponse{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	raid := &RaidResponse{}
+	resp.HydrateResponseCommon(&raid.ResponseCommon)
+
+	return raid, nil
+}
+
+// CancelRaidResponse is the response from StartRaid
+type CancelRaidResponse struct {
+	ResponseCommon
+}
+
+// CancelRaidParams are the parameters for CancelRaid
+type CancelRaidParams struct {
+	BroadcasterID string `query:"broadcaster_id"`
+}
+
+// CancelRaid cancels a pending raid.
+// Required scope: channel:manage:raids
+// Rate limit: 10 requests within a 10-minute window.
+func (c *Client) CancelRaid(params *CancelRaidParams) (*CancelRaidResponse, error) {
+	resp, err := c.delete("/raids", nil, params)
+	if err != nil {
+		return nil, err
+	}
+
+	canceledRaid := &CancelRaidResponse{}
+	resp.HydrateResponseCommon(&canceledRaid.ResponseCommon)
+
+	return canceledRaid, nil
+}


Test patch:
diff --git a/raids_test.go b/raids_test.go
new file mode 100644
index 0000000..5c79873
--- /dev/null
+++ b/raids_test.go
@@ -0,0 +1,165 @@
+package helix
+
+import (
+	"context"
+	"net/http"
+	"testing"
+)
+
+func TestStartRaid(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode        int
+		options           *Options
+		FromBroadcasterID string
+		ToBroadcasterID   string
+		respBody          string
+	}{
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			"237509153",
+			"237509153",
+			`{"data":[{"created_at": "2022-02-18T07:20:50.52Z","is_mature": false}]}`,
+		},
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			"237509153",
+			"237509153",
+			`{"error":"Bad Request","status":400,"message":"The IDs in \"from_broadcaster_id\" and \"to_broadcaster_id\" cannot be the same ID."}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.StartRaid(&StartRaidParams{
+			FromBroadcasterID: testCase.FromBroadcasterID,
+			ToBroadcasterID:   testCase.ToBroadcasterID,
+		})
+		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 := "The IDs in \"from_broadcaster_id\" and \"to_broadcaster_id\" cannot be the same ID."
+			if resp.ErrorMessage != expectedErrMsg {
+				t.Errorf("expected error message to be %s, got %s", expectedErrMsg, resp.ErrorMessage)
+			}
+
+			continue
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+		ctx:  context.Background(),
+	}
+
+	_, err := c.StartRaid(&StartRaidParams{})
+	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")
+	}
+}
+
+func TestCancelRaid(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode    int
+		options       *Options
+		BroadcasterID string
+		respBody      string
+	}{
+		{
+			http.StatusNoContent,
+			&Options{ClientID: "my-client-id"},
+			"237509153",
+			"",
+		},
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			"237509153",
+			`{"error":"Bad Request","status":400,"message":"The ID in the \"broadcaster_id query\" parameter is not valid."}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.CancelRaid(&CancelRaidParams{
+			BroadcasterID: testCase.BroadcasterID,
+		})
+		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 := "The ID in the \"broadcaster_id query\" parameter is not valid."
+			if resp.ErrorMessage != expectedErrMsg {
+				t.Errorf("expected error message to be %s, got %s", expectedErrMsg, resp.ErrorMessage)
+			}
+
+			continue
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+		ctx:  context.Background(),
+	}
+
+	_, err := c.CancelRaid(&CancelRaidParams{})
+	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")
+	}
+}