Repository: nicklaw5/helix
Pull request #107 (2021-07-10 07:48:34)

Update EventSubCondition to support 'extension_client_id'
Some Twitch EventSub subscriptions require the condition field 'extension_client_id' field to be populated 
- Extension Bits Transaction Create Condition
- see [here](https://dev.twitch.tv/docs/eventsub/eventsub-reference)

- Enforced EventSub Secret length validation to adhere to Twitch requirements
- updated 'TestCreateEventSubSubscriptions' unit tests
   - add 409 conflict error 'subscription already exists'
   - add 400 bad request invalid secret length error
   - add unit test  for code coverage for enforced 'https' callback error


Patch:
diff --git a/eventsub.go b/eventsub.go
index 301e332..ad96a5c 100644
--- a/eventsub.go
+++ b/eventsub.go
@@ -31,6 +31,7 @@ type EventSubCondition struct {
 	ToBroadcasterUserID   string `json:"to_broadcaster_user_id"`
 	RewardID              string `json:"reward_id"`
 	ClientID              string `json:"client_id"`
+	ExtensionClientID     string `json:"extension_client_id"`
 	UserID                string `json:"user_id"`
 }
 
@@ -557,6 +558,11 @@ func (c *Client) CreateEventSubSubscription(payload *EventSubSubscription) (*Eve
 	if payload.Transport.Method == "webhook" && !strings.HasPrefix(payload.Transport.Callback, "https://") {
 		return nil, fmt.Errorf("error: callback must use https")
 	}
+
+	if payload.Transport.Secret != "" && (len(payload.Transport.Secret) < 10 || len(payload.Transport.Secret) > 100) {
+		return nil, fmt.Errorf("error: secret must be between 10 and 100 characters")
+	}
+
 	callbackUrl, err := url.Parse(payload.Transport.Callback)
 	if err != nil {
 		return nil, err


Test patch:
diff --git a/eventsub_test.go b/eventsub_test.go
index 91b9dc8..3e24787 100644
--- a/eventsub_test.go
+++ b/eventsub_test.go
@@ -189,16 +189,72 @@ func TestCreateEventSubSubscriptions(t *testing.T) {
 	t.Parallel()
 
 	testCases := []struct {
-		statusCode int
-		options    *Options
-		params     *EventSubSubscription
-		respBody   string
+		statusCode    int
+		options       *Options
+		params        *EventSubSubscription
+		respBody      string
+		validationErr string
 	}{
 		{
 			http.StatusUnauthorized,
 			&Options{ClientID: "my-client-id"},
 			&EventSubSubscription{},
 			`{"error":"Unauthorized","status":401,"message":"OAuth token is missing"}`,
+			"",
+		},
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&EventSubSubscription{
+				Type:    "channel.follow",
+				Version: "1",
+				Condition: EventSubCondition{
+					BroadcasterUserID: "12345678",
+				},
+				Transport: EventSubTransport{
+					Method:   "webhook",
+					Callback: "https://example.com/eventsub/follow",
+					Secret:   "111",
+				},
+			},
+			`{"error":"Bad Request","status":400,"message":"secret must be between 10 and 100 characters"}`,
+			"error: secret must be between 10 and 100 characters",
+		},
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&EventSubSubscription{
+				Type:    "channel.follow",
+				Version: "1",
+				Condition: EventSubCondition{
+					BroadcasterUserID: "12345678",
+				},
+				Transport: EventSubTransport{
+					Method:   "webhook",
+					Callback: "http://example.com/eventsub/follow",
+					Secret:   "s3cr37w0rd",
+				},
+			},
+			`{"error":"Bad Request","status":400,"message":"call back must be https protocol"}`,
+			"error: callback must use https",
+		},
+		{
+			http.StatusConflict,
+			&Options{ClientID: "my-client-id"},
+			&EventSubSubscription{
+				Type:    "channel.follow",
+				Version: "1",
+				Condition: EventSubCondition{
+					BroadcasterUserID: "12345678",
+				},
+				Transport: EventSubTransport{
+					Method:   "webhook",
+					Callback: "https://example.com/eventsub/follow",
+					Secret:   "s3cr37w0rd",
+				},
+			},
+			`{"error":"Conflict","status":409,"message":"subscription already exists"}`,
+			"",
 		},
 		{
 			http.StatusOK,
@@ -216,6 +272,7 @@ func TestCreateEventSubSubscriptions(t *testing.T) {
 				},
 			},
 			`{"data":[{"id":"4d06fabc-4cf4-4e99-a60f-b457d5c69305","status":"webhook_callback_verification_pending","type":"channel.follow","version":"1","condition":{"broadcaster_user_id":"12345678"},"created_at":"2021-03-10T23:38:50.311154721Z","transport":{"method":"webhook","callback":"https://example.com/eventsub/follow"},"cost":1}],"limit":10000,"total":1,"max_total_cost":10000,"total_cost":1}`,
+			"",
 		},
 	}
 
@@ -224,6 +281,10 @@ func TestCreateEventSubSubscriptions(t *testing.T) {
 
 		resp, err := c.CreateEventSubSubscription(testCase.params)
 		if err != nil {
+			if err.Error() == testCase.validationErr {
+				continue
+			}
+
 			t.Error(err)
 		}
 
@@ -247,6 +308,22 @@ func TestCreateEventSubSubscriptions(t *testing.T) {
 
 			continue
 		}
+		if resp.StatusCode == http.StatusConflict {
+			if resp.Error != "Conflict" {
+				t.Errorf("expected error to be \"%s\", got \"%s\"", "Conflict", resp.Error)
+			}
+
+			if resp.ErrorStatus != http.StatusConflict {
+				t.Errorf("expected error status to be \"%d\", got \"%d\"", http.StatusUnauthorized, resp.ErrorStatus)
+			}
+
+			expectedErrMsg := "subscription already exists"
+			if resp.ErrorMessage != expectedErrMsg {
+				t.Errorf("expected error message to be \"%s\", got \"%s\"", expectedErrMsg, resp.ErrorMessage)
+			}
+
+			continue
+		}
 		if len(resp.Data.EventSubSubscriptions) != 1 {
 			t.Errorf("expected result length to be \"%d\", got \"%d\"", 1, len(resp.Data.EventSubSubscriptions))