package panel import ( "path/filepath" "strings" "testing" "time" "github.com/xlzd/gotp" "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/util/crypto" ) // install.sh replaces the credentials only while this reports true, and before // the first start a database holds no user at all. func TestHasDefaultCredential(t *testing.T) { hash := func(t *testing.T, password string) string { t.Helper() h, err := crypto.HashPasswordAsBcrypt(password) if err != nil { t.Fatalf("hash: %v", err) } return h } cases := []struct { name string user *model.User want bool }{ {name: "no user before the first start", user: nil, want: true}, {name: "legacy admin/admin", user: &model.User{Username: "admin", Password: hash(t, "admin")}, want: true}, {name: "chosen credentials", user: &model.User{Username: "admin", Password: hash(t, "s3cret-Pass")}, want: false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) if err := database.GetDB().Where("1 = 1").Delete(&model.User{}).Error; err != nil { t.Fatalf("clear users: %v", err) } if tc.user != nil { if err := database.GetDB().Create(tc.user).Error; err != nil { t.Fatalf("create user: %v", err) } } got, err := (&UserService{}).HasDefaultCredential() if err != nil || got != tc.want { t.Fatalf("HasDefaultCredential() = %v, %v; want %v", got, err, tc.want) } }) } } // The login route refuses names over MaxUsernameLength unread, so storing one would lock // the admin out of their own panel. func TestUserSettersRefuseNamesTheLoginRouteRefuses(t *testing.T) { dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) svc := &UserService{} if err := svc.UpdateFirstUser("admin", "s3cret-Pass"); err != nil { t.Fatalf("UpdateFirstUser: %v", err) } var user model.User if err := database.GetDB().First(&user).Error; err != nil { t.Fatalf("load user: %v", err) } long := strings.Repeat("a", MaxUsernameLength+1) if err := svc.UpdateFirstUser(long, "s3cret-Pass"); err == nil { t.Fatal("UpdateFirstUser stored a name the login route refuses") } if err := svc.UpdateUser(user.Id, long, "s3cret-Pass"); err == nil { t.Fatal("UpdateUser stored a name the login route refuses") } var stored model.User if err := database.GetDB().First(&stored, user.Id).Error; err != nil || stored.Username != "admin" { t.Fatalf("stored username = %q (%v), want it unchanged", stored.Username, err) } } // A stored 2FA secret in a form gotp cannot decode made every login panic (HTTP 500). func TestCheckUserTwoFactorSecretForms(t *testing.T) { cases := []struct { name, stored, codeSecret string want bool }{ {"canonical", "JBSWY3DPEHPK3PXP", "JBSWY3DPEHPK3PXP", true}, {"lower case", "jbswy3dpehpk3pxp", "JBSWY3DPEHPK3PXP", true}, {"not base32", "totp-secret", "JBSWY3DPEHPK3PXP", false}, {"empty secret", "", "", false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) svc := &UserService{} if err := svc.UpdateFirstUser("admin", "s3cret-Pass"); err != nil { t.Fatalf("UpdateFirstUser: %v", err) } if err := svc.settingService.SetTwoFactorEnable(true); err != nil { t.Fatal(err) } if err := svc.settingService.SetTwoFactorToken(tc.stored); err != nil { t.Fatal(err) } code := gotp.NewDefaultTOTP(tc.codeSecret).AtTime(time.Now()) user, err := svc.CheckUser("admin", "s3cret-Pass", code) if got := err == nil && user != nil; got != tc.want { t.Fatalf("CheckUser with stored secret %q: ok = %v (%v), want %v", tc.stored, got, err, tc.want) } }) } }