package handlers import ( "encoding/json" "net/http" "net/http/httptest" "testing" "time" "veza-backend-api/internal/models" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func setupUpgradeCreatorTest(t *testing.T) (*gin.Engine, *gorm.DB, uuid.UUID) { gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) db.Exec("PRAGMA foreign_keys = ON") require.NoError(t, db.AutoMigrate(&models.User{})) userID := uuid.New() // Default seed: verified user with role=user. Individual tests can override. require.NoError(t, db.Create(&models.User{ ID: userID, Username: "upgrader", Email: "upgrade@example.com", Role: "user", IsVerified: true, }).Error) logger := zaptest.NewLogger(t) router := gin.New() router.Use(func(c *gin.Context) { c.Set("user_id", userID); c.Next() }) router.POST("/users/me/upgrade-creator", UpgradeToCreator(db, nil, logger)) return router, db, userID } func postUpgrade(t *testing.T, router *gin.Engine) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodPost, "/users/me/upgrade-creator", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) return w } func TestUpgradeToCreator_VerifiedUserIsPromoted(t *testing.T) { router, db, userID := setupUpgradeCreatorTest(t) w := postUpgrade(t, router) assert.Equal(t, http.StatusOK, w.Code) var resp UpgradeCreatorResponse require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "creator", resp.Role) assert.False(t, resp.AlreadyElevated) require.NotNil(t, resp.PromotedToCreatorAt) assert.WithinDuration(t, time.Now(), *resp.PromotedToCreatorAt, 5*time.Second) var after models.User require.NoError(t, db.First(&after, "id = ?", userID).Error) assert.Equal(t, "creator", after.Role) require.NotNil(t, after.PromotedToCreatorAt) } func TestUpgradeToCreator_UnverifiedIsRefused(t *testing.T) { router, db, userID := setupUpgradeCreatorTest(t) // Flip is_verified=false; default seed had it true. require.NoError(t, db.Model(&models.User{}).Where("id = ?", userID).Update("is_verified", false).Error) w := postUpgrade(t, router) assert.Equal(t, http.StatusForbidden, w.Code) assert.Contains(t, w.Body.String(), "EMAIL_NOT_VERIFIED") var after models.User require.NoError(t, db.First(&after, "id = ?", userID).Error) assert.Equal(t, "user", after.Role, "role must not flip when email is not verified") assert.Nil(t, after.PromotedToCreatorAt) } func TestUpgradeToCreator_AlreadyCreatorIsIdempotent(t *testing.T) { router, db, userID := setupUpgradeCreatorTest(t) earlier := time.Now().Add(-48 * time.Hour).UTC() require.NoError(t, db.Model(&models.User{}).Where("id = ?", userID).Updates(map[string]interface{}{ "role": "creator", "promoted_to_creator_at": earlier, }).Error) w := postUpgrade(t, router) assert.Equal(t, http.StatusOK, w.Code) var resp UpgradeCreatorResponse require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "creator", resp.Role) assert.True(t, resp.AlreadyElevated) require.NotNil(t, resp.PromotedToCreatorAt) assert.WithinDuration(t, earlier, *resp.PromotedToCreatorAt, time.Second, "idempotent path must preserve the original promotion timestamp") } func TestUpgradeToCreator_AdminIsIdempotentWithoutTimestamp(t *testing.T) { router, db, userID := setupUpgradeCreatorTest(t) // Admin was assigned out-of-band — no promoted_to_creator_at. require.NoError(t, db.Model(&models.User{}).Where("id = ?", userID).Update("role", "admin").Error) w := postUpgrade(t, router) assert.Equal(t, http.StatusOK, w.Code) var resp UpgradeCreatorResponse require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "admin", resp.Role, "admin must keep their role, not be downgraded to creator") assert.True(t, resp.AlreadyElevated) assert.Nil(t, resp.PromotedToCreatorAt, "admin assigned without the self-service flow has no timestamp") } func TestUpgradeToCreator_UserNotFound(t *testing.T) { gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&models.User{})) logger := zaptest.NewLogger(t) router := gin.New() // Inject a user_id that doesn't exist in the DB router.Use(func(c *gin.Context) { c.Set("user_id", uuid.New()); c.Next() }) router.POST("/users/me/upgrade-creator", UpgradeToCreator(db, nil, logger)) w := postUpgrade(t, router) assert.Equal(t, http.StatusNotFound, w.Code) assert.Contains(t, w.Body.String(), "USER_NOT_FOUND") } func TestUpgradeToCreator_NoAuthContext(t *testing.T) { gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&models.User{})) logger := zaptest.NewLogger(t) router := gin.New() // No middleware that sets user_id router.POST("/users/me/upgrade-creator", UpgradeToCreator(db, nil, logger)) w := postUpgrade(t, router) assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Contains(t, w.Body.String(), "UNAUTHORIZED") }