package handlers import ( "context" "encoding/json" "net/http" "net/http/httptest" "sync" "testing" "time" "everypublish/server/internal/models" "everypublish/server/internal/platform" "github.com/gin-gonic/gin" gormsqlite "gorm.io/driver/sqlite" "gorm.io/gorm" ) type loginTestAdapter struct { mu sync.Mutex polls int saved string } func (a *loginTestAdapter) Platform() string { return "test-platform" } func (a *loginTestAdapter) BeginLogin(context.Context) (platform.LoginSession, error) { return platform.LoginSession{Token: "token-1", URL: "https://qr.test/1"}, nil } func (a *loginTestAdapter) PollLogin(context.Context, string) (platform.LoginPoll, error) { a.mu.Lock() defer a.mu.Unlock() a.polls++ if a.polls == 1 { return platform.LoginPoll{State: platform.LoginPending, Prompt: "等待扫码"}, nil } return platform.LoginPoll{State: platform.LoginConfirmed, Cookies: "cookie-value"}, nil } func (a *loginTestAdapter) SaveCredentials(_ uint64, _ uint64, cookies string) error { a.mu.Lock() a.saved = cookies a.mu.Unlock() return nil } func (a *loginTestAdapter) Publish(context.Context, platform.PublishInput) (platform.PublishResult, error) { return platform.PublishResult{}, nil } func TestAccountBindPollsAdapterAndActivatesAccount(t *testing.T) { db, err := gorm.Open(gormsqlite.Open(t.TempDir()+"/account.db"), &gorm.Config{}) if err != nil { t.Fatal(err) } if err = db.AutoMigrate(&models.Account{}, &models.Challenge{}, &models.Notification{}, &models.AuditLog{}); err != nil { t.Fatal(err) } account := models.Account{WorkspaceID: 9, Platform: "test-platform", Status: "unbound"} if err = db.Create(&account).Error; err != nil { t.Fatal(err) } adapter := &loginTestAdapter{} registry := platform.NewRegistry() if err = registry.Register(adapter); err != nil { t.Fatal(err) } h := &AccountHandler{DB: db, Registry: registry, ExecutorMode: "web", PollInterval: 5 * time.Millisecond} gin.SetMode(gin.TestMode) req := httptest.NewRequest(http.MethodPost, "/accounts/1/bind", nil) recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) ctx.Request = req ctx.Params = gin.Params{{Key: "id", Value: "1"}} ctx.Set("wsid", uint64(9)) ctx.Set("uid", uint64(3)) h.Bind(ctx) if recorder.Code != http.StatusOK { t.Fatalf("bind status=%d body=%s", recorder.Code, recorder.Body.String()) } var envelope struct { Data struct { ChallengeID uint64 `json:"challengeId"` QRURL string `json:"qrUrl"` } `json:"data"` } if err = json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil { t.Fatal(err) } if envelope.Data.ChallengeID == 0 || envelope.Data.QRURL != "https://qr.test/1" { t.Fatalf("unexpected bind response: %s", recorder.Body.String()) } deadline := time.Now().Add(time.Second) for time.Now().Before(deadline) { var got models.Account _ = db.First(&got, account.ID).Error if got.Status == "active" { adapter.mu.Lock() saved := adapter.saved adapter.mu.Unlock() if saved != "cookie-value" { t.Fatalf("credentials not saved: %q", saved) } var challenge models.Challenge _ = db.First(&challenge, envelope.Data.ChallengeID).Error if challenge.Status != "solved" { t.Fatalf("challenge status=%s", challenge.Status) } return } time.Sleep(10 * time.Millisecond) } t.Fatal("adapter login worker did not activate account") }