mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-27 09:42:10 +03:00
12d51d7195
Most tests opened a throwaway panel DB with database.InitDB, which runs the full AutoMigrate + seed on an empty file every time: ~230ms, and ~850ms under -race because GORM's reflection-heavy migration is what the detector slows most. internal/web/service does this in ~550 of its 830 tests, so the CI race job spent ~10 of its ~14.6 minutes re-migrating empty databases. internal/database/dbtest.InitDB migrates once per test process, then hands each test its own copy of that file (~130ms under -race) and registers the CloseDB cleanup. The copy then goes through InitDB like a panel restart, so every test still starts from the state a fresh install has. Tests that reopen an existing file, migrate a hand-built legacy DB or target Postgres keep calling database.InitDB. Locally under -race: internal/web/service 626s (last CI run) -> 114s, internal/sub 246s -> 35s.
116 lines
3.5 KiB
Go
116 lines
3.5 KiB
Go
package panel
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/config"
|
|
"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"
|
|
)
|
|
|
|
var errInjectedTokenCreate = errors.New("injected token create failure")
|
|
|
|
func TestApiTokenCreatedAtSeconds(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in int64
|
|
want int64
|
|
}{
|
|
{name: "seconds", in: 1_782_485_394, want: 1_782_485_394},
|
|
{name: "legacy milliseconds", in: 1_782_485_394_270, want: 1_782_485_394},
|
|
{name: "unset", in: 0, want: 0},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := apiTokenCreatedAtSeconds(tt.in); got != tt.want {
|
|
t.Fatalf("apiTokenCreatedAtSeconds(%d) = %d, want %d", tt.in, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRecreateByNamePreservesTokenWhenReplacementFails(t *testing.T) {
|
|
t.Setenv("XUI_DB_FOLDER", t.TempDir())
|
|
dbtest.InitDB(t, config.GetDBPath())
|
|
|
|
svc := ApiTokenService{}
|
|
first, err := svc.RecreateByName("cli-fallback")
|
|
if err != nil {
|
|
t.Fatalf("first recreate: %v", err)
|
|
}
|
|
db := database.GetDB()
|
|
const callback = "test:fail-token-replacement"
|
|
if err := db.Callback().Create().Before("gorm:create").Register(callback, func(tx *gorm.DB) {
|
|
if token, ok := tx.Statement.Dest.(*model.ApiToken); ok && token.Name == "cli-fallback" {
|
|
tx.AddError(errInjectedTokenCreate)
|
|
}
|
|
}); err != nil {
|
|
t.Fatalf("register callback: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Callback().Create().Remove(callback) })
|
|
|
|
if _, err := svc.RecreateByName("cli-fallback"); !errors.Is(err, errInjectedTokenCreate) {
|
|
t.Fatalf("recreate error = %v, want %v", err, errInjectedTokenCreate)
|
|
}
|
|
var row model.ApiToken
|
|
if err := db.Where("name = ?", "cli-fallback").First(&row).Error; err != nil {
|
|
t.Fatalf("load preserved token: %v", err)
|
|
}
|
|
if !svc.Match(first.Token) {
|
|
t.Fatal("original token was revoked after replacement failure")
|
|
}
|
|
}
|
|
|
|
// Create caps the name at 64 characters; RecreateByName writes the same column
|
|
// and now takes operator input from -tokenName, so it must cap it too.
|
|
func TestRecreateByNameRejectsOverlongName(t *testing.T) {
|
|
t.Setenv("XUI_DB_FOLDER", t.TempDir())
|
|
dbtest.InitDB(t, config.GetDBPath())
|
|
|
|
const wantErr = "token name must be 64 characters or fewer"
|
|
|
|
svc := ApiTokenService{}
|
|
_, err := svc.RecreateByName(strings.Repeat("n", 65))
|
|
if err == nil {
|
|
t.Fatal("expected a 65-character token name to be rejected")
|
|
}
|
|
if got := strings.TrimSpace(err.Error()); got != wantErr {
|
|
t.Fatalf("error = %q, want %q — any other error would pass a bare nil check", got, wantErr)
|
|
}
|
|
if _, err := svc.RecreateByName(strings.Repeat("n", 64)); err != nil {
|
|
t.Fatalf("64 characters is the documented limit, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRecreateByNameKeepsOneToken(t *testing.T) {
|
|
t.Setenv("XUI_DB_FOLDER", t.TempDir())
|
|
dbtest.InitDB(t, config.GetDBPath())
|
|
|
|
svc := ApiTokenService{}
|
|
first, err := svc.RecreateByName("cli-fallback")
|
|
if err != nil {
|
|
t.Fatalf("first recreate: %v", err)
|
|
}
|
|
second, err := svc.RecreateByName("cli-fallback")
|
|
if err != nil {
|
|
t.Fatalf("second recreate: %v", err)
|
|
}
|
|
if first.Token == second.Token {
|
|
t.Fatal("second call returned the same plaintext, want a rotated token")
|
|
}
|
|
|
|
var count int64
|
|
if err := database.GetDB().Model(model.ApiToken{}).Where("name = ?", "cli-fallback").Count(&count).Error; err != nil {
|
|
t.Fatalf("count: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("token rows = %d, want 1", count)
|
|
}
|
|
}
|