Repository: nicklaw5/helix
Pull request #197 (2023-07-18 00:28:49)

add schedule endpoints
#192
* Get Channel Stream Schedule
* Update Channel Stream Schedule
* Create Channel Stream Schedule Segment
* Update Channel Stream Schedule Segment
* Delete Channel Stream Schedule Segment
I only missed `Get Channel iCalendar`


Patch:
diff --git a/SUPPORTED_ENDPOINTS.md b/SUPPORTED_ENDPOINTS.md
index 788421a..8d3fb22 100644
--- a/SUPPORTED_ENDPOINTS.md
+++ b/SUPPORTED_ENDPOINTS.md
@@ -96,12 +96,12 @@
 - [x] End Prediction
 - [x] Start a raid
 - [x] Cancel a raid
-- [ ] Get Channel Stream Schedule
+- [x] Get Channel Stream Schedule
 - [ ] Get Channel iCalendar
-- [ ] Update Channel Stream Schedule
-- [ ] Create Channel Stream Schedule Segment
-- [ ] Update Channel Stream Schedule Segment
-- [ ] Delete Channel Stream Schedule Segment
+- [x] Update Channel Stream Schedule
+- [x] Create Channel Stream Schedule Segment
+- [x] Update Channel Stream Schedule Segment
+- [x] Delete Channel Stream Schedule Segment
 - [x] Search Categories
 - [ ] Search Channels
 - [x] Get Stream Key
diff --git a/schedule.go b/schedule.go
new file mode 100644
index 0000000..35d0750
--- /dev/null
+++ b/schedule.go
@@ -0,0 +1,187 @@
+package helix
+
+// GetScheduleParams are the parameters for GetSchedule
+type GetScheduleParams struct {
+	BroadcasterID string `json:"broadcaster_id"`
+	ID            string `json:"id"`
+	StartTime     Time   `json:"start_time"`
+	UTCOffset     string `json:"utc_offset"`
+	First         int    `json:"first"`
+	After         string `json:"after"`
+}
+
+// GetScheduleResponse is the response data in GetSchedule
+type GetScheduleResponse struct {
+	ResponseCommon
+
+	Data GetScheduleData
+}
+
+type GetScheduleData struct {
+	Schedule   ScheduleData          `json:"data"`
+	Pagination GetSchedulePagination `json:"pagination"`
+}
+
+type ScheduleData struct {
+	Segments         []GetScheduleSegment `json:"segments"`
+	BroadcasterID    string               `json:"broadcaster_id"`
+	BroadcasterName  string               `json:"broadcaster_name"`
+	BroadcasterLogin string               `json:"broadcaster_login"`
+	Vacation         GetScheduleVacation  `json:"vacation"`
+}
+
+type GetScheduleSegment struct {
+	ID            string                     `json:"id"`
+	StartTime     Time                       `json:"start_time"`
+	EndTime       Time                       `json:"end_time"`
+	Title         string                     `json:"title"`
+	CanceledUntil string                     `json:"canceled_until"`
+	Category      GetScheduleSegmentCategory `json:"category"`
+	IsRecurring   bool                       `json:"is_recurring"`
+}
+
+type GetScheduleSegmentCategory struct {
+	ID   string `json:"id"`
+	Name string `json:"name"`
+}
+
+type GetScheduleVacation struct {
+	StartTime Time `json:"start_time"`
+	EndTime   Time `json:"end_time"`
+}
+
+type GetSchedulePagination struct {
+	Cursor string `json:"cursor"`
+}
+
+// Gets the broadcaster’s streaming schedule.
+// You can get the entire schedule or specific segments of the schedule
+func (c *Client) GetSchedule(params *GetScheduleParams) (*GetScheduleResponse, error) {
+	resp, err := c.get("/schedule", &GetScheduleData{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	schedule := &GetScheduleResponse{}
+	resp.HydrateResponseCommon(&schedule.ResponseCommon)
+	schedule.Data.Schedule = resp.Data.(*GetScheduleData).Schedule
+	schedule.Data.Pagination = resp.Data.(*GetScheduleData).Pagination
+
+	return schedule, nil
+}
+
+type UpdateScheduleParams struct {
+	BroadcasterID     string `json:"broadcaster_id"`
+	IsVacationEnabled bool   `json:"is_vacation_enabled"`
+	VacationStartTime Time   `json:"vacation_start_time"`
+	VacationEndTime   Time   `json:"vacation_end_time"`
+	Timezone          string `json:"timezone"`
+}
+
+type UpdateScheduleResponse struct {
+	ResponseCommon
+}
+
+// Updates the broadcaster’s schedule settings, such as scheduling a vacation
+func (c *Client) UpdateSchedule(params *UpdateScheduleParams) (*UpdateScheduleResponse, error) {
+	resp, err := c.get("/schedule/settings", nil, params)
+	if err != nil {
+		return nil, err
+	}
+
+	schedule := &UpdateScheduleResponse{}
+	resp.HydrateResponseCommon(&schedule.ResponseCommon)
+
+	return schedule, nil
+}
+
+type CreateScheduleSegmentParams struct {
+	BroadcasterID string `json:"broadcaster_id"`
+	StartTime     Time   `json:"start_time"`
+	Timezone      string `json:"timezone"`
+	Duration      string `json:"duration"`
+	IsRecurring   bool   `json:"is_recurring"`
+	CategoryID    string `json:"category_id"`
+	Title         string `json:"title"`
+}
+
+type CreateScheduleSegmentResponse struct {
+	ResponseCommon
+
+	Data CreateScheduleSegmentData
+}
+
+type CreateScheduleSegmentData struct {
+	Schedule ScheduleData `json:"data"`
+}
+
+// Updates the broadcaster’s schedule settings, such as scheduling a vacation
+func (c *Client) CreateScheduleSegment(params *CreateScheduleSegmentParams) (*CreateScheduleSegmentResponse, error) {
+	resp, err := c.post("/schedule/segment", &CreateScheduleSegmentData{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	schedule := &CreateScheduleSegmentResponse{}
+	resp.HydrateResponseCommon(&schedule.ResponseCommon)
+	schedule.Data.Schedule = resp.Data.(*CreateScheduleSegmentData).Schedule
+
+	return schedule, nil
+}
+
+type UpdateScheduleSegmentParams struct {
+	BroadcasterID string `json:"broadcaster_id"`
+	ID            string `json:"id"`
+	StartTime     Time   `json:"start_time"`
+	Duration      string `json:"duration"`
+	CategoryID    string `json:"category_id"`
+	Title         string `json:"title"`
+	IsCanceled    bool   `json:"is_canceled"`
+	Timezone      string `json:"timezone"`
+}
+
+type UpdateScheduleSegmentResponse struct {
+	ResponseCommon
+
+	Data UpdateScheduleSegmentData
+}
+
+type UpdateScheduleSegmentData struct {
+	Schedule ScheduleData `json:"data"`
+}
+
+// Updates the broadcaster’s schedule settings, such as scheduling a vacation
+func (c *Client) UpdateScheduleSegment(params *UpdateScheduleSegmentParams) (*UpdateScheduleSegmentResponse, error) {
+	resp, err := c.patchAsJSON("/schedule/segment", &UpdateScheduleSegmentData{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	schedule := &UpdateScheduleSegmentResponse{}
+	resp.HydrateResponseCommon(&schedule.ResponseCommon)
+	schedule.Data.Schedule = resp.Data.(*UpdateScheduleSegmentData).Schedule
+
+	return schedule, nil
+}
+
+type DeleteScheduleSegmentParams struct {
+	BroadcasterID string `json:"broadcaster_id"`
+	ID            string `json:"id"`
+}
+
+type DeleteScheduleSegmentResponse struct {
+	ResponseCommon
+}
+
+// Removes a broadcast segment from the broadcaster’s streaming schedule
+func (c *Client) DeleteScheduleSegment(params *DeleteScheduleSegmentParams) (*DeleteScheduleSegmentResponse, error) {
+	resp, err := c.delete("/schedule/segment", nil, params)
+	if err != nil {
+		return nil, err
+	}
+
+	schedule := &DeleteScheduleSegmentResponse{}
+	resp.HydrateResponseCommon(&schedule.ResponseCommon)
+
+	return schedule, nil
+}


Test patch:
diff --git a/schedule_test.go b/schedule_test.go
new file mode 100644
index 0000000..3f22544
--- /dev/null
+++ b/schedule_test.go
@@ -0,0 +1,370 @@
+package helix
+
+import (
+	"context"
+	"net/http"
+	"testing"
+)
+
+func TestGetSchedule(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode int
+		options    *Options
+		params     *GetScheduleParams
+		respBody   string
+	}{
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id", UserAccessToken: "my-access-token"},
+			&GetScheduleParams{
+				BroadcasterID: "141981764",
+			},
+			`{
+				"data": {
+				  "segments": [
+					{
+					  "id": "eyJzZWdtZW50SUQiOiJlNGFjYzcyNC0zNzFmLTQwMmMtODFjYS0yM2FkYTc5NzU5ZDQiLCJpc29ZZWFyIjoyMDIxLCJpc29XZWVrIjoyNn0=",
+					  "start_time": "2021-07-01T18:00:00Z",
+					  "end_time": "2021-07-01T19:00:00Z",
+					  "title": "TwitchDev Monthly Update // July 1, 2021",
+					  "canceled_until": null,
+					  "category": {
+						  "id": "509670",
+						  "name": "Science & Technology"
+					  },
+					  "is_recurring": false
+					}
+				  ],
+				  "broadcaster_id": "141981764",
+				  "broadcaster_name": "TwitchDev",
+				  "broadcaster_login": "twitchdev",
+				  "vacation": null
+				},
+				"pagination": {}
+			}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.GetSchedule(testCase.params)
+		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.StatusOK {
+			if resp.Data.Schedule.BroadcasterID != testCase.params.BroadcasterID {
+				t.Errorf("Expected broadcasterID = %s, got %s", testCase.params.BroadcasterID, resp.Data.Schedule.BroadcasterID)
+			}
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+		ctx:  context.Background(),
+	}
+
+	_, err := c.GetSchedule(&GetScheduleParams{})
+	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 TestUpdateSchedule(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode int
+		options    *Options
+		params     *UpdateScheduleParams
+	}{
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id", UserAccessToken: "my-access-token"},
+			&UpdateScheduleParams{
+				BroadcasterID:     "141981764",
+				IsVacationEnabled: true,
+				Timezone:          "America/New_York",
+			},
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, ``, nil))
+
+		resp, err := c.UpdateSchedule(testCase.params)
+		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)
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+		ctx:  context.Background(),
+	}
+
+	_, err := c.UpdateSchedule(&UpdateScheduleParams{})
+	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 TestCreateScheduleSegment(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode int
+		options    *Options
+		params     *CreateScheduleSegmentParams
+		respBody   string
+	}{
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id", UserAccessToken: "my-access-token"},
+			&CreateScheduleSegmentParams{
+				BroadcasterID: "141981764",
+				Timezone:      "America/New_York",
+				IsRecurring:   false,
+				Duration:      "60",
+				CategoryID:    "509670",
+				Title:         "TwitchDev Monthly Update // July 1, 2021",
+			},
+			`{
+				"data": {
+				  "segments": [
+					{
+					  "id": "eyJzZWdtZW50SUQiOiJlNGFjYzcyNC0zNzFmLTQwMmMtODFjYS0yM2FkYTc5NzU5ZDQiLCJpc29ZZWFyIjoyMDIxLCJpc29XZWVrIjoyNn0=",
+					  "start_time": "2021-07-01T18:00:00Z",
+					  "end_time": "2021-07-01T19:00:00Z",
+					  "title": "TwitchDev Monthly Update // July 1, 2021",
+					  "canceled_until": null,
+					  "category": {
+						  "id": "509670",
+						  "name": "Science & Technology"
+					  },
+					  "is_recurring": false
+					}
+				  ],
+				  "broadcaster_id": "141981764",
+				  "broadcaster_name": "TwitchDev",
+				  "broadcaster_login": "twitchdev",
+				  "vacation": null
+				}
+			}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.CreateScheduleSegment(testCase.params)
+		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.StatusOK {
+			if resp.Data.Schedule.BroadcasterID != testCase.params.BroadcasterID {
+				t.Errorf("Expected broadcasterID = %s, got %s", testCase.params.BroadcasterID, resp.Data.Schedule.BroadcasterID)
+			}
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+		ctx:  context.Background(),
+	}
+
+	_, err := c.CreateScheduleSegment(&CreateScheduleSegmentParams{})
+	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 TestUpdateScheduleSegment(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode int
+		options    *Options
+		params     *UpdateScheduleSegmentParams
+		respBody   string
+	}{
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id", UserAccessToken: "my-access-token"},
+			&UpdateScheduleSegmentParams{
+				ID:            "eyJzZWdtZW50SUQiOiJlNGFjYzcyNC0zNzFmLTQwMmMtODFjYS0yM2FkYTc5NzU5ZDQiLCJpc29ZZWFyIjoyMDIxLCJpc29XZWVrIjoyNn0=",
+				BroadcasterID: "141981764",
+				Duration:      "120",
+			},
+			`{
+				"data": {
+				  "segments": [
+					{
+					  "id": "eyJzZWdtZW50SUQiOiJlNGFjYzcyNC0zNzFmLTQwMmMtODFjYS0yM2FkYTc5NzU5ZDQiLCJpc29ZZWFyIjoyMDIxLCJpc29XZWVrIjoyNn0=",
+					  "start_time": "2021-07-01T18:00:00Z",
+					  "end_time": "2021-07-01T20:00:00Z",
+					  "title": "TwitchDev Monthly Update // July 1, 2021",
+					  "canceled_until": null,
+					  "category": {
+						  "id": "509670",
+						  "name": "Science & Technology"
+					  },
+					  "is_recurring": false
+					}
+				  ],
+				  "broadcaster_id": "141981764",
+				  "broadcaster_name": "TwitchDev",
+				  "broadcaster_login": "twitchdev",
+				  "vacation": null
+				}
+			}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.UpdateScheduleSegment(testCase.params)
+		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.StatusOK {
+			if resp.Data.Schedule.BroadcasterID != testCase.params.BroadcasterID {
+				t.Errorf("Expected broadcasterID = %s, got %s", testCase.params.BroadcasterID, resp.Data.Schedule.BroadcasterID)
+			}
+			if len(resp.Data.Schedule.Segments) > 0 {
+				if resp.Data.Schedule.Segments[0].ID != testCase.params.ID {
+					t.Errorf("Expected ID = %s, got %s", testCase.params.ID, resp.Data.Schedule.Segments[0].ID)
+				}
+			}
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+		ctx:  context.Background(),
+	}
+
+	_, err := c.UpdateScheduleSegment(&UpdateScheduleSegmentParams{})
+	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 TestDeleteScheduleSegment(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode int
+		options    *Options
+		params     *DeleteScheduleSegmentParams
+	}{
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id", UserAccessToken: "my-access-token"},
+			&DeleteScheduleSegmentParams{
+				BroadcasterID: "141981764",
+				ID:            "eyJzZWdtZW50SUQiOiI4Y2EwN2E2NC0xYTZkLTRjYWItYWE5Ni0xNjIyYzNjYWUzZDkiLCJpc29ZZWFyIjoyMDIxLCJpc29XZWVrIjoyMX0=",
+			},
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, ``, nil))
+
+		resp, err := c.DeleteScheduleSegment(testCase.params)
+		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)
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+		ctx:  context.Background(),
+	}
+
+	_, err := c.DeleteScheduleSegment(&DeleteScheduleSegmentParams{})
+	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")
+	}
+}