Repository: slack-go/slack
Pull request #1266 (2024-02-20 18:31:05)

Parse string or boolean for SlashCommand.IsEnterpriseInstall
This PR adds a custom JSON parser for the `SlashCommand` struct to work correctly when `"is_enterprise_install"` comes in as either a string or a boolean. As noted in the issue, in socket mode, certain (but not all 🤷) payloads have `"is_enterprise_install"` as a quoted boolean (e.g. `"false"`) whereas others have it as an actual boolean (e.g. `false`).

This doesn't update the `IsEnterpriseInstall` field in `SlashCommand` itself, so this change should be transparent to anyone who might have started using that newly added field.

Fixes https://github.com/slack-go/slack/issues/1265
Prior fix attempt: https://github.com/slack-go/slack/pull/1264

Testing:
1. Added unit test covering happy paths and error cases
2. My application works correctly pointing at this branch


Patch:
diff --git a/slash.go b/slash.go
index a41b767..fd46abf 100644
--- a/slash.go
+++ b/slash.go
@@ -1,7 +1,10 @@
 package slack
 
 import (
+	"encoding/json"
+	"fmt"
 	"net/http"
+	"strconv"
 )
 
 // SlashCommand contains information about a request of the slash command
@@ -55,3 +58,34 @@ func (s SlashCommand) ValidateToken(verificationTokens ...string) bool {
 	}
 	return false
 }
+
+// UnmarshalJSON handles is_enterprise_install being either a boolean or a
+// string when parsing JSON from various payloads
+func (s *SlashCommand) UnmarshalJSON(data []byte) error {
+	type SlashCommandCopy SlashCommand
+	scopy := &struct {
+		*SlashCommandCopy
+		IsEnterpriseInstall interface{} `json:"is_enterprise_install"`
+	}{
+		SlashCommandCopy: (*SlashCommandCopy)(s),
+	}
+
+	if err := json.Unmarshal(data, scopy); err != nil {
+		return err
+	}
+
+	switch rawValue := scopy.IsEnterpriseInstall.(type) {
+	case string:
+		b, err := strconv.ParseBool(rawValue)
+		if err != nil {
+			return fmt.Errorf("parsing boolean for is_enterprise_install: %w", err)
+		}
+		s.IsEnterpriseInstall = b
+	case bool:
+		s.IsEnterpriseInstall = rawValue
+	default:
+		return fmt.Errorf("wrong data type for is_enterprise_install: %T", scopy.IsEnterpriseInstall)
+	}
+
+	return nil
+}


Test patch:
diff --git a/slash_test.go b/slash_test.go
index e5c79e9..e068fc4 100644
--- a/slash_test.go
+++ b/slash_test.go
@@ -1,6 +1,7 @@
 package slack
 
 import (
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -99,3 +100,70 @@ func TestSlash_ServeHTTP(t *testing.T) {
 		resp.Body.Close()
 	}
 }
+
+func TestSlash_UnmarshalJSON(t *testing.T) {
+	tests := []struct {
+		body                    string
+		wantIsEnterpriseInstall bool
+		wantToken               string
+		wantUnmarshalError      string
+	}{
+		{
+			body:                    `{"token":"blahblah","is_enterprise_install":"false"}`,
+			wantIsEnterpriseInstall: false,
+			wantToken:               "blahblah",
+			wantUnmarshalError:      "",
+		},
+		{
+			body:                    `{"token":"blahblah","is_enterprise_install":false}`,
+			wantIsEnterpriseInstall: false,
+			wantToken:               "blahblah",
+			wantUnmarshalError:      "",
+		},
+		{
+			body:                    `{"token":"blahblah","is_enterprise_install":"true"}`,
+			wantIsEnterpriseInstall: true,
+			wantToken:               "blahblah",
+			wantUnmarshalError:      "",
+		},
+		{
+			body:                    `{"token":"blahblah","is_enterprise_install":true}`,
+			wantIsEnterpriseInstall: true,
+			wantToken:               "blahblah",
+			wantUnmarshalError:      "",
+		},
+		{
+			body:               `{"token":"blahblah","is_enterprise_install":42}`,
+			wantUnmarshalError: "wrong data type for is_enterprise_install: float64",
+		},
+		{
+			body:               `{"token":"blahblah","is_enterprise_install":"unconvertable to bool"}`,
+			wantUnmarshalError: "parsing boolean for is_enterprise_install: strconv.ParseBool: parsing \"unconvertable to bool\": invalid syntax",
+		},
+	}
+
+	for i, test := range tests {
+		var result SlashCommand
+
+		err := json.Unmarshal([]byte(test.body), &result)
+		if err != nil {
+			if err.Error() != test.wantUnmarshalError {
+				t.Errorf("%d: Got error %v, want error %q", i, err, test.wantUnmarshalError)
+			}
+			continue
+		}
+
+		if test.wantUnmarshalError != "" {
+			t.Errorf("%d: Got no error, want error %q", i, test.wantUnmarshalError)
+			continue
+		}
+
+		if result.IsEnterpriseInstall != test.wantIsEnterpriseInstall {
+			t.Errorf("%d: Got IsEnterpriseInstall %v, want IsEnterpriseInstall %v", i, result.IsEnterpriseInstall, test.wantIsEnterpriseInstall)
+		}
+
+		if result.Token != test.wantToken {
+			t.Errorf("%d: Got Token %v, want Token %v", i, result.Token, test.wantToken)
+		}
+	}
+}