package common import ( "context" "errors" "regexp" "testing" "github.com/DATA-DOG/go-sqlmock" "github.com/alicebob/miniredis/v2" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/pkg/xerr" "github.com/redis/go-redis/v9" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/schema" ) func TestIsUserDisabled(t *testing.T) { trueValue := true falseValue := false testCases := []struct { name string user *user.User want bool }{ { name: "nil user treated as enabled", user: nil, want: false, }, { name: "nil enable treated as enabled", user: &user.User{Id: 1}, want: false, }, { name: "enabled user", user: &user.User{Id: 2, Enable: &trueValue}, want: false, }, { name: "disabled user", user: &user.User{Id: 3, Enable: &falseValue}, want: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { if got := IsUserDisabled(tc.user); got != tc.want { t.Fatalf("IsUserDisabled() = %v, want %v", got, tc.want) } }) } } func TestResolveEnabledUser(t *testing.T) { t.Run("cache hit false returns disabled error without db query", func(t *testing.T) { svcCtx, mock, redisServer := newEnableTestServiceContext(t) defer redisServer.Close() ctx := context.Background() if err := svcCtx.Redis.Set(ctx, UserEnableCacheKey(9), "false", 0).Err(); err != nil { t.Fatalf("seed redis: %v", err) } _, err := ResolveEnabledUser(ctx, svcCtx, 9) assertCodeError(t, err, xerr.UserDisabled) if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("unexpected db query: %v", err) } }) t.Run("cache miss loads enabled user and backfills cache", func(t *testing.T) { svcCtx, mock, redisServer := newEnableTestServiceContext(t) defer redisServer.Close() expectFindOne(mock, 11, true) ctx := context.Background() userInfo, err := ResolveEnabledUser(ctx, svcCtx, 11) if err != nil { t.Fatalf("ResolveEnabledUser() error = %v", err) } if userInfo.Id != 11 { t.Fatalf("ResolveEnabledUser() user id = %d, want 11", userInfo.Id) } cached, err := svcCtx.Redis.Get(ctx, UserEnableCacheKey(11)).Result() if err != nil { t.Fatalf("read backfilled cache: %v", err) } if cached != "true" { t.Fatalf("backfilled cache = %q, want %q", cached, "true") } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("db expectations: %v", err) } }) t.Run("cache miss loads disabled user and caches false", func(t *testing.T) { svcCtx, mock, redisServer := newEnableTestServiceContext(t) defer redisServer.Close() expectFindOne(mock, 13, false) ctx := context.Background() _, err := ResolveEnabledUser(ctx, svcCtx, 13) assertCodeError(t, err, xerr.UserDisabled) cached, cacheErr := svcCtx.Redis.Get(ctx, UserEnableCacheKey(13)).Result() if cacheErr != nil { t.Fatalf("read disabled cache: %v", cacheErr) } if cached != "false" { t.Fatalf("disabled cache = %q, want %q", cached, "false") } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("db expectations: %v", err) } }) t.Run("redis failure falls back to db", func(t *testing.T) { svcCtx, mock, redisServer := newEnableTestServiceContext(t) expectFindOne(mock, 17, true) redisServer.Close() userInfo, err := ResolveEnabledUser(context.Background(), svcCtx, 17) if err != nil { t.Fatalf("ResolveEnabledUser() error = %v", err) } if userInfo.Id != 17 { t.Fatalf("ResolveEnabledUser() user id = %d, want 17", userInfo.Id) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("db expectations: %v", err) } }) } func newEnableTestServiceContext(t *testing.T) (*svc.ServiceContext, sqlmock.Sqlmock, *miniredis.Miniredis) { t.Helper() sqlDB, mock, err := sqlmock.New() if err != nil { t.Fatalf("sqlmock.New() error = %v", err) } mock.MatchExpectationsInOrder(false) t.Cleanup(func() { _ = sqlDB.Close() }) gdb, err := gorm.Open(mysql.New(mysql.Config{ Conn: sqlDB, SkipInitializeWithVersion: true, }), &gorm.Config{ NamingStrategy: schema.NamingStrategy{SingularTable: true}, }) if err != nil { t.Fatalf("gorm.Open() error = %v", err) } redisServer, err := miniredis.Run() if err != nil { t.Fatalf("miniredis.Run() error = %v", err) } rdb := redis.NewClient(&redis.Options{Addr: redisServer.Addr()}) t.Cleanup(func() { _ = rdb.Close() }) return &svc.ServiceContext{ DB: gdb, Redis: rdb, UserModel: user.NewModel(gdb, rdb), }, mock, redisServer } func expectFindOne(mock sqlmock.Sqlmock, userID int64, enabled bool) { rows := sqlmock.NewRows([]string{ "id", "password", "algo", "salt", "avatar", "balance", "refer_code", "referer_id", "commission", "referral_percentage", "only_first_purchase", "gift_amount", "enable", "is_admin", "enable_balance_notify", "enable_login_notify", "enable_subscribe_notify", "enable_trade_notify", "rules", "member_status", "remark", "last_login_time", "created_at", "updated_at", "deleted_at", }).AddRow( userID, "pwd", "default", "", "", int64(0), "", int64(0), int64(0), uint8(0), true, int64(0), enabled, false, false, false, false, false, "", "", "", nil, nil, nil, nil, ) mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `user` WHERE `id` = ? ORDER BY `user`.`id` LIMIT ?")). WithArgs(userID, 1). WillReturnRows(rows) mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `user_device` WHERE `user_device`.`user_id` = ?")). WithArgs(userID). WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "identifier"})) mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `user_auth_methods` WHERE `user_auth_methods`.`user_id` = ?")). WithArgs(userID). WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "auth_type", "auth_identifier", "verified"})) } func assertCodeError(t *testing.T, err error, wantCode uint32) { t.Helper() if err == nil { t.Fatal("expected error, got nil") } var codeErr *xerr.CodeError if !errors.As(err, &codeErr) { t.Fatalf("error %T does not contain xerr.CodeError: %v", err, err) } if codeErr.GetErrCode() != wantCode { t.Fatalf("error code = %d, want %d", codeErr.GetErrCode(), wantCode) } }