package v1 import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/auth" "github.com/gochat/gochat/internal/config" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" "github.com/gochat/gochat/pkg/crypto" ) func setupChatwootAuthTest(t *testing.T) (*gin.Engine, *gorm.DB, *model.User) { t.Helper() gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&model.Account{}, &model.User{}, &model.AccountUser{}, &model.AccessToken{}, &model.UserSession{})) account := &model.Account{Name: "Auth Account", Status: "active", OnboardingStep: "invite_team"} require.NoError(t, db.Create(account).Error) passwordDigest, err := crypto.HashPassword("password123") require.NoError(t, err) confirmedAt := time.Now().UTC() user := &model.User{ AccountID: account.ID, Name: "Auth User", Email: "auth@example.com", UID: "auth@example.com", PasswordDigest: passwordDigest, Provider: "email", DisplayName: "Auth Display", MessageSignature: "Cheers", PubsubToken: "pubsub-auth-user", ConfirmedAt: &confirmedAt, Active: true, } require.NoError(t, db.Create(user).Error) require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "administrator", Availability: "online", AutoOffline: true}).Error) require.NoError(t, db.Create(&model.AccessToken{OwnerType: model.AccessTokenOwnerTypeUser, OwnerID: user.ID, Token: "auth-profile-token", TokenPrefix: "auth-pro", Name: "Personal Access Token"}).Error) jwtCfg := &config.JWTConfig{Secret: "auth-test-secret", ExpiryHours: 1, RefreshExpiryHours: 24} jwtSvc := auth.NewJWTService(jwtCfg) refreshStore := auth.NewRefreshTokenStore(nil, jwtCfg) authSvc := service.NewAuthService(db, jwtSvc, refreshStore) profileSvc := service.NewProfileService(repository.NewUserRepo(db), repository.NewAccountUserRepo(db), repository.NewAccessTokenRepo(db)) handler := NewAuthHandler(authSvc, profileSvc).WithSecureCookies(true) router := gin.New() RegisterChatwootAuthRoutes(router.Group("/auth"), handler) return router, db, user } func TestRegisterAuthRoutesDoesNotExposeSelfRegistration(t *testing.T) { router := gin.New() RegisterAuthRoutes(router.Group("/api/v1"), &AuthHandler{}) for _, route := range router.Routes() { require.NotEqual(t, "/api/v1/auth/register", route.Path) } } func TestChatwootAuthSignInReturnsDeviseHeadersAndUserPayload(t *testing.T) { router, db, user := setupChatwootAuthTest(t) body, _ := json.Marshal(map[string]string{"email": " AUTH@example.com ", "password": "password123"}) req, _ := http.NewRequest(http.MethodPost, "/auth/sign_in", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) Chrome/126.0.0.0 Safari/537.36") w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) require.NotEmpty(t, w.Header().Get("access-token")) require.Equal(t, "Bearer", w.Header().Get("token-type")) require.NotEmpty(t, w.Header().Get("client")) require.NotEmpty(t, w.Header().Get("expiry")) require.Equal(t, "auth@example.com", w.Header().Get("uid")) var payload map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload)) data := payload["data"].(map[string]any) assertChatwootAuthUserFixture(t, data) var session model.UserSession require.NoError(t, db.Where("user_id = ? AND client_id = ?", user.ID, w.Header().Get("client")).First(&session).Error) require.Equal(t, "Chrome", session.BrowserName) require.Equal(t, "Linux", session.PlatformName) } func assertChatwootAuthUserFixture(t *testing.T, data map[string]any) { t.Helper() require.Equal(t, "Auth User", data["name"]) require.Equal(t, "auth@example.com", data["email"]) require.Equal(t, "auth@example.com", data["uid"]) require.Equal(t, "Auth Display", data["available_name"]) require.Equal(t, "Auth Display", data["display_name"]) require.Equal(t, "", data["avatar_url"]) require.Equal(t, "User", data["type"]) require.Equal(t, "email", data["provider"]) require.Equal(t, "pubsub-auth-user", data["pubsub_token"]) require.Equal(t, "Cheers", data["message_signature"]) require.Equal(t, "auth-profile-token", data["access_token"]) require.Equal(t, "administrator", data["role"]) require.Equal(t, map[string]any{}, data["custom_attributes"]) require.Equal(t, map[string]any{}, data["ui_settings"]) require.Equal(t, true, data["confirmed"]) require.NotNil(t, data["account_id"]) accounts := data["accounts"].([]any) require.Len(t, accounts, 1) account := accounts[0].(map[string]any) require.Equal(t, "Auth Account", account["name"]) require.Equal(t, "active", account["status"]) require.Equal(t, "invite_team", account["onboarding_step"]) require.Equal(t, "administrator", account["role"]) require.Equal(t, "online", account["availability"]) require.Equal(t, "online", account["availability_status"]) require.Equal(t, true, account["auto_offline"]) require.Equal(t, []any{"administrator"}, account["permissions"]) } func TestChatwootAuthValidateTokenReturnsPayloadData(t *testing.T) { router, _, _ := setupChatwootAuthTest(t) token := signInAndReturnAccessToken(t, router) req, _ := http.NewRequest(http.MethodGet, "/auth/validate_token", nil) req.Header.Set("access-token", token) w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var body map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) payload := body["payload"].(map[string]any) require.Equal(t, true, payload["success"]) data := payload["data"].(map[string]any) assertChatwootAuthUserFixture(t, data) } func TestChatwootBrowserSessionReloadAndLogout(t *testing.T) { router, db, user := setupChatwootAuthTest(t) body, _ := json.Marshal(map[string]string{"email": "auth@example.com", "password": "password123"}) signIn := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/auth/sign_in", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(signIn, req) require.Equal(t, http.StatusOK, signIn.Code, signIn.Body.String()) refreshCookie := responseCookie(t, signIn, browserRefreshCookie) markerCookie := responseCookie(t, signIn, browserSessionMarker) require.True(t, refreshCookie.HttpOnly) require.True(t, refreshCookie.Secure) require.Equal(t, http.SameSiteLaxMode, refreshCookie.SameSite) require.False(t, markerCookie.HttpOnly) require.NotContains(t, signIn.Header().Get("Set-Cookie"), "cw_d_session_info") reload := httptest.NewRecorder() reloadReq := httptest.NewRequest(http.MethodGet, "/auth/validate_token", nil) reloadReq.AddCookie(refreshCookie) reloadReq.AddCookie(markerCookie) router.ServeHTTP(reload, reloadReq) require.Equal(t, http.StatusOK, reload.Code, reload.Body.String()) require.NotEmpty(t, reload.Header().Get("access-token")) rotatedRefresh := responseCookie(t, reload, browserRefreshCookie) require.NotEqual(t, refreshCookie.Value, rotatedRefresh.Value) logout := httptest.NewRecorder() logoutReq := httptest.NewRequest(http.MethodDelete, "/auth/sign_out", nil) logoutReq.Header.Set("access-token", reload.Header().Get("access-token")) logoutReq.AddCookie(rotatedRefresh) router.ServeHTTP(logout, logoutReq) require.Equal(t, http.StatusOK, logout.Code, logout.Body.String()) require.Less(t, responseCookie(t, logout, browserRefreshCookie).MaxAge, 0) var sessions int64 require.NoError(t, db.Model(&model.UserSession{}).Where("user_id = ?", user.ID).Count(&sessions).Error) require.Zero(t, sessions) replay := httptest.NewRecorder() replayReq := httptest.NewRequest(http.MethodGet, "/auth/validate_token", nil) replayReq.AddCookie(rotatedRefresh) router.ServeHTTP(replay, replayReq) require.Equal(t, http.StatusUnauthorized, replay.Code) } func responseCookie(t *testing.T, recorder *httptest.ResponseRecorder, name string) *http.Cookie { t.Helper() for _, cookie := range recorder.Result().Cookies() { if cookie.Name == name { return cookie } } t.Fatalf("missing response cookie %s", name) return nil } func TestChatwootAuthValidateTokenSerializesPlatformAdminType(t *testing.T) { router, db, user := setupChatwootAuthTest(t) require.NoError(t, db.Model(user).Updates(map[string]any{ "role": "super_admin", "type": "User", }).Error) token := signInAndReturnAccessToken(t, router) req, _ := http.NewRequest(http.MethodGet, "/auth/validate_token", nil) req.Header.Set("access-token", token) w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var body map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) data := body["payload"].(map[string]any)["data"].(map[string]any) require.Equal(t, "SuperAdmin", data["type"]) require.Equal(t, "administrator", data["role"]) } func TestChatwootAuthSignOutRevokesRefreshSession(t *testing.T) { router, _, _ := setupChatwootAuthTest(t) token := signInAndReturnAccessToken(t, router) req, _ := http.NewRequest(http.MethodDelete, "/auth/sign_out", nil) req.Header.Set("access-token", token) w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var body map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) require.Equal(t, true, body["success"]) } func TestChatwootAuthResetPasswordReturnsReferenceMessage(t *testing.T) { router, db, user := setupChatwootAuthTest(t) body, _ := json.Marshal(map[string]string{"email": "auth@example.com"}) req, _ := http.NewRequest(http.MethodPost, "/auth/password", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var resp map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) require.Equal(t, service.ChatwootPasswordResetMessage, resp["message"]) var updated model.User require.NoError(t, db.First(&updated, user.ID).Error) require.NotEmpty(t, updated.ResetPasswordToken) } func TestChatwootAuthConfirmResetPasswordReturnsAuthPayload(t *testing.T) { router, db, user := setupChatwootAuthTest(t) require.NoError(t, db.Model(user).Updates(map[string]interface{}{ "reset_password_token": "raw-reset-token", "reset_password_sent_at": time.Now().UTC(), }).Error) body, _ := json.Marshal(map[string]string{ "reset_password_token": "raw-reset-token", "password": "newpassword", "password_confirmation": "newpassword", }) req, _ := http.NewRequest(http.MethodPut, "/auth/password", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) require.NotEmpty(t, w.Header().Get("access-token")) var resp map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) data := resp["data"].(map[string]any) require.Equal(t, "auth@example.com", data["email"]) var updated model.User require.NoError(t, db.First(&updated, user.ID).Error) require.True(t, crypto.CheckPassword("newpassword", updated.PasswordDigest)) require.Empty(t, updated.ResetPasswordToken) } func TestChatwootAuthConfirmationReturnsAuthPayload(t *testing.T) { router, db, user := setupChatwootAuthTest(t) require.NoError(t, db.Model(user).Updates(map[string]interface{}{ "confirmation_token": "raw-confirm-token", "confirmed_at": nil, }).Error) body, _ := json.Marshal(map[string]string{"confirmation_token": "raw-confirm-token"}) req, _ := http.NewRequest(http.MethodPost, "/auth/confirmation", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) require.NotEmpty(t, w.Header().Get("access-token")) var updated model.User require.NoError(t, db.First(&updated, user.ID).Error) require.NotNil(t, updated.ConfirmedAt) require.Empty(t, updated.ConfirmationToken) } func signInAndReturnAccessToken(t *testing.T, router *gin.Engine) string { t.Helper() body, _ := json.Marshal(map[string]string{"email": "auth@example.com", "password": "password123"}) req, _ := http.NewRequest(http.MethodPost, "/auth/sign_in", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) return w.Header().Get("access-token") }