package panel import ( "path/filepath" "testing" "time" "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/util/crypto" ) func setupAPITokenTestDB(t *testing.T) { t.Helper() dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { t.Fatalf("InitDB: %v", err) } t.Cleanup(func() { if err := database.CloseDB(); err != nil { t.Fatalf("CloseDB: %v", err) } }) } func TestAPITokenScopeExpiryAndExpectedRevoke(t *testing.T) { setupAPITokenTestDB(t) svc := &ApiTokenService{} future := time.Now().Add(time.Hour).UnixMilli() created, err := svc.Create("node-1", model.ApiScopeNodeSync, future) if err != nil { t.Fatalf("create node-sync token: %v", err) } row, ok := svc.MatchToken(created.Token) if !ok { t.Fatal("fresh node-sync token did not match") } if row.Scope != model.ApiScopeNodeSync || row.ExpiresAt != future { t.Fatalf("matched row scope/expiry = %s/%d, want node-sync/%d", row.Scope, row.ExpiresAt, future) } if _, err := svc.Create("bad-scope", "superuser", 0); err == nil { t.Fatal("unknown scope must be rejected on create") } if _, err := svc.Create("past", model.ApiScopeAdmin, time.Now().Add(-time.Minute).UnixMilli()); err == nil { t.Fatal("past expiry must be rejected on create") } const expiredPlain = "expired-token" if err := database.GetDB().Create(&model.ApiToken{ Name: "expired", Token: crypto.HashTokenSHA256(expiredPlain), Enabled: true, Scope: model.ApiScopeAdmin, ExpiresAt: time.Now().Add(-time.Minute).UnixMilli(), }).Error; err != nil { t.Fatalf("seed expired token: %v", err) } if _, ok := svc.MatchToken(expiredPlain); ok { t.Fatal("expired token must fail closed") } const unknownPlain = "unknown-scope-token" if err := database.GetDB().Create(&model.ApiToken{ Name: "unknown", Token: crypto.HashTokenSHA256(unknownPlain), Enabled: true, Scope: "root", }).Error; err != nil { t.Fatalf("seed unknown-scope token: %v", err) } if _, ok := svc.MatchToken(unknownPlain); ok { t.Fatal("unknown token scope must fail closed") } if err := svc.DisableExpectedScope(created.Id, model.ApiScopeAdmin); err == nil { t.Fatal("expected-scope revoke must refuse a node-sync token when admin was expected") } if _, ok := svc.MatchToken(created.Token); !ok { t.Fatal("wrong expected-scope revoke disabled the token") } if err := svc.DisableExpectedScope(created.Id, model.ApiScopeNodeSync); err != nil { t.Fatalf("disable expected node-sync token: %v", err) } if _, ok := svc.MatchToken(created.Token); ok { t.Fatal("disabled token still matched") } } func TestAPITokenDeleteAndEnableRequireExpectedScope(t *testing.T) { setupAPITokenTestDB(t) svc := &ApiTokenService{} created, err := svc.Create("scoped", model.ApiScopeMonitor, 0) if err != nil { t.Fatalf("Create: %v", err) } if err := svc.SetEnabledExpectedScope(created.Id, model.ApiScopeAdmin, false); err == nil { t.Fatal("wrong expected scope changed token state") } if _, ok := svc.MatchToken(created.Token); !ok { t.Fatal("wrong-scope update disabled token") } if err := svc.DeleteExpectedScope(created.Id, model.ApiScopeAdmin); err == nil { t.Fatal("wrong expected scope deleted token") } if err := svc.DeleteExpectedScope(created.Id, model.ApiScopeMonitor); err != nil { t.Fatalf("DeleteExpectedScope: %v", err) } } func TestAPITokenEmptyExpectedScopeCannotTargetAdmin(t *testing.T) { setupAPITokenTestDB(t) svc := &ApiTokenService{} created, err := svc.Create("admin-token", model.ApiScopeAdmin, 0) if err != nil { t.Fatalf("Create: %v", err) } if err := svc.DisableExpectedScope(created.Id, ""); err == nil { t.Fatal("empty expected scope defaulted to admin") } if _, ok := svc.MatchToken(created.Token); !ok { t.Fatal("empty expected scope disabled the admin token") } } func TestAPITokenAdditiveDefaultsPreserveLegacyAccess(t *testing.T) { setupAPITokenTestDB(t) const plaintext = "legacy-token" if err := database.GetDB().Exec( "INSERT INTO api_tokens (name, token, enabled, created_at) VALUES (?, ?, ?, ?)", "legacy", crypto.HashTokenSHA256(plaintext), true, time.Now().Unix(), ).Error; err != nil { t.Fatalf("insert legacy-shaped token: %v", err) } row, ok := (&ApiTokenService{}).MatchToken(plaintext) if !ok { t.Fatal("legacy-shaped token no longer authenticates") } if row.Scope != model.ApiScopeAdmin || row.ExpiresAt != 0 { t.Fatalf("legacy defaults = scope %q expiry %d, want admin/0", row.Scope, row.ExpiresAt) } }