1
0

api_token_test.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. package panel
  2. import (
  3. "errors"
  4. "strings"
  5. "testing"
  6. "gorm.io/gorm"
  7. "github.com/mhsanaei/3x-ui/v3/internal/config"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  10. )
  11. var errInjectedTokenCreate = errors.New("injected token create failure")
  12. func TestApiTokenCreatedAtSeconds(t *testing.T) {
  13. tests := []struct {
  14. name string
  15. in int64
  16. want int64
  17. }{
  18. {name: "seconds", in: 1_782_485_394, want: 1_782_485_394},
  19. {name: "legacy milliseconds", in: 1_782_485_394_270, want: 1_782_485_394},
  20. {name: "unset", in: 0, want: 0},
  21. }
  22. for _, tt := range tests {
  23. t.Run(tt.name, func(t *testing.T) {
  24. if got := apiTokenCreatedAtSeconds(tt.in); got != tt.want {
  25. t.Fatalf("apiTokenCreatedAtSeconds(%d) = %d, want %d", tt.in, got, tt.want)
  26. }
  27. })
  28. }
  29. }
  30. func TestRecreateByNamePreservesTokenWhenReplacementFails(t *testing.T) {
  31. t.Setenv("XUI_DB_FOLDER", t.TempDir())
  32. if err := database.InitDB(config.GetDBPath()); err != nil {
  33. t.Fatalf("init db: %v", err)
  34. }
  35. t.Cleanup(func() { _ = database.CloseDB() })
  36. svc := ApiTokenService{}
  37. first, err := svc.RecreateByName("cli-fallback")
  38. if err != nil {
  39. t.Fatalf("first recreate: %v", err)
  40. }
  41. db := database.GetDB()
  42. const callback = "test:fail-token-replacement"
  43. if err := db.Callback().Create().Before("gorm:create").Register(callback, func(tx *gorm.DB) {
  44. if token, ok := tx.Statement.Dest.(*model.ApiToken); ok && token.Name == "cli-fallback" {
  45. tx.AddError(errInjectedTokenCreate)
  46. }
  47. }); err != nil {
  48. t.Fatalf("register callback: %v", err)
  49. }
  50. t.Cleanup(func() { _ = db.Callback().Create().Remove(callback) })
  51. if _, err := svc.RecreateByName("cli-fallback"); !errors.Is(err, errInjectedTokenCreate) {
  52. t.Fatalf("recreate error = %v, want %v", err, errInjectedTokenCreate)
  53. }
  54. var row model.ApiToken
  55. if err := db.Where("name = ?", "cli-fallback").First(&row).Error; err != nil {
  56. t.Fatalf("load preserved token: %v", err)
  57. }
  58. if !svc.Match(first.Token) {
  59. t.Fatal("original token was revoked after replacement failure")
  60. }
  61. }
  62. // Create caps the name at 64 characters; RecreateByName writes the same column
  63. // and now takes operator input from -tokenName, so it must cap it too.
  64. func TestRecreateByNameRejectsOverlongName(t *testing.T) {
  65. t.Setenv("XUI_DB_FOLDER", t.TempDir())
  66. if err := database.InitDB(config.GetDBPath()); err != nil {
  67. t.Fatalf("init db: %v", err)
  68. }
  69. t.Cleanup(func() { _ = database.CloseDB() })
  70. const wantErr = "token name must be 64 characters or fewer"
  71. svc := ApiTokenService{}
  72. _, err := svc.RecreateByName(strings.Repeat("n", 65))
  73. if err == nil {
  74. t.Fatal("expected a 65-character token name to be rejected")
  75. }
  76. if got := strings.TrimSpace(err.Error()); got != wantErr {
  77. t.Fatalf("error = %q, want %q — any other error would pass a bare nil check", got, wantErr)
  78. }
  79. if _, err := svc.RecreateByName(strings.Repeat("n", 64)); err != nil {
  80. t.Fatalf("64 characters is the documented limit, got: %v", err)
  81. }
  82. }
  83. func TestRecreateByNameKeepsOneToken(t *testing.T) {
  84. t.Setenv("XUI_DB_FOLDER", t.TempDir())
  85. if err := database.InitDB(config.GetDBPath()); err != nil {
  86. t.Fatalf("init db: %v", err)
  87. }
  88. t.Cleanup(func() { _ = database.CloseDB() })
  89. svc := ApiTokenService{}
  90. first, err := svc.RecreateByName("cli-fallback")
  91. if err != nil {
  92. t.Fatalf("first recreate: %v", err)
  93. }
  94. second, err := svc.RecreateByName("cli-fallback")
  95. if err != nil {
  96. t.Fatalf("second recreate: %v", err)
  97. }
  98. if first.Token == second.Token {
  99. t.Fatal("second call returned the same plaintext, want a rotated token")
  100. }
  101. var count int64
  102. if err := database.GetDB().Model(model.ApiToken{}).Where("name = ?", "cli-fallback").Count(&count).Error; err != nil {
  103. t.Fatalf("count: %v", err)
  104. }
  105. if count != 1 {
  106. t.Fatalf("token rows = %d, want 1", count)
  107. }
  108. }