package v1 import ( "encoding/json" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupTwitterAuthorizationRouter() *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() r.RedirectTrailingSlash = false handler := NewTwitterChannelHandler(nil, nil, nil, nil) r.POST("/api/v1/accounts/:account_id/twitter/authorization", handler.ChatwootAuthorization) return r } func TestTwitterAuthorization_BadAccountID(t *testing.T) { r := setupTwitterAuthorizationRouter() w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/abc/twitter/authorization", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) errBody := resp["error"].(map[string]interface{}) assert.Contains(t, errBody["message"], "invalid account_id") } func TestTwitterAuthorization_ReturnsChatwootPayload(t *testing.T) { t.Setenv("TWITTER_CONSUMER_KEY", "twitter-key") t.Setenv("TWITTER_CONSUMER_SECRET", "twitter-secret") t.Setenv("TWITTER_OAUTH_REQUEST_TOKEN_URL", "https://oauth.example.test/request_token") t.Setenv("TWITTER_API_BASE_URL", "https://api.twitter.test") t.Setenv("FRONTEND_URL", "https://app.example.test/") originalClient := twitterAuthorizationHTTPClient twitterAuthorizationHTTPClient = &http.Client{Transport: twitterAuthorizationRoundTripFunc(func(req *http.Request) (*http.Response, error) { assert.Equal(t, http.MethodPost, req.Method) assert.Equal(t, "https://oauth.example.test/request_token", req.URL.String()) authHeader := req.Header.Get("Authorization") assert.Contains(t, authHeader, "OAuth ") assert.Contains(t, authHeader, `oauth_consumer_key="twitter-key"`) assert.Contains(t, authHeader, "oauth_callback=") return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("oauth_token=request-token&oauth_token_secret=request-secret")), Header: http.Header{}}, nil })} t.Cleanup(func() { twitterAuthorizationHTTPClient = originalClient }) r := setupTwitterAuthorizationRouter() w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/42/twitter/authorization", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, true, resp["success"]) assert.Equal(t, "https://api.twitter.test/oauth/authorize?oauth_token=request-token", resp["url"]) } func TestTwitterAuthorization_SignedStateMatchesCallback(t *testing.T) { t.Setenv("TWITTER_CONSUMER_KEY", "twitter-key") t.Setenv("TWITTER_CONSUMER_SECRET", "twitter-secret") t.Setenv("TWITTER_OAUTH_REQUEST_TOKEN_URL", "https://oauth.example.test/request_token") t.Setenv("FRONTEND_URL", "https://app.example.test") originalClient := twitterAuthorizationHTTPClient var callbackURL string twitterAuthorizationHTTPClient = &http.Client{Transport: twitterAuthorizationRoundTripFunc(func(req *http.Request) (*http.Response, error) { authHeader := req.Header.Get("Authorization") for _, part := range strings.Split(strings.TrimPrefix(authHeader, "OAuth "), ",") { part = strings.TrimSpace(part) if strings.HasPrefix(part, "oauth_callback=") { callbackURL = strings.Trim(strings.TrimPrefix(part, "oauth_callback="), `"`) decoded, err := url.QueryUnescape(callbackURL) require.NoError(t, err) callbackURL = decoded } } return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("oauth_token=request-token")), Header: http.Header{}}, nil })} t.Cleanup(func() { twitterAuthorizationHTTPClient = originalClient }) r := setupTwitterAuthorizationRouter() w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/42/twitter/authorization", nil) r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) parsed, err := url.Parse(callbackURL) require.NoError(t, err) assert.Equal(t, "https://app.example.test/twitter/callback", parsed.Scheme+"://"+parsed.Host+parsed.Path) claims := jwt.MapClaims{} token, err := jwt.ParseWithClaims(parsed.Query().Get("state"), claims, func(token *jwt.Token) (any, error) { return []byte("twitter-secret"), nil }) require.NoError(t, err) require.True(t, token.Valid) assert.Equal(t, float64(42), claims["sub"]) } func TestTwitterAuthorization_NotConfigured(t *testing.T) { t.Setenv("TWITTER_CONSUMER_KEY", "") t.Setenv("TWITTER_CONSUMER_SECRET", "") r := setupTwitterAuthorizationRouter() w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/42/twitter/authorization", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnprocessableEntity, w.Code) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, false, resp["success"]) } type twitterAuthorizationRoundTripFunc func(*http.Request) (*http.Response, error) func (f twitterAuthorizationRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) }