package model import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" ) func TestExternalIdentityClaimEnforcesSingleOwnerAtomically(t *testing.T) { truncateTables(t) first := User{Username: "telegram-owner-one", Password: "password", AffCode: "telegram-owner-one"} second := User{Username: "telegram-owner-two", Password: "password", AffCode: "telegram-owner-two"} require.NoError(t, DB.Create(&first).Error) require.NoError(t, DB.Create(&second).Error) require.NoError(t, DB.Transaction(func(tx *gorm.DB) error { return ClaimExternalIdentityWithTx(tx, ExternalIdentityProviderTelegram, "telegram-123", first.Id) })) err := DB.Transaction(func(tx *gorm.DB) error { return ClaimExternalIdentityWithTx(tx, ExternalIdentityProviderTelegram, "telegram-123", second.Id) }) assert.ErrorIs(t, err, ErrExternalIdentityAlreadyClaimed) err = DB.Transaction(func(tx *gorm.DB) error { return ClaimExternalIdentityWithTx(tx, ExternalIdentityProviderTelegram, "telegram-456", first.Id) }) assert.ErrorIs(t, err, ErrExternalIdentityAlreadyClaimed) var claims []ExternalIdentityClaim require.NoError(t, DB.Find(&claims).Error) require.Len(t, claims, 1) assert.Equal(t, first.Id, claims[0].UserId) assert.Equal(t, "telegram-123", claims[0].Subject) require.NoError(t, DB.Transaction(func(tx *gorm.DB) error { return ReleaseExternalIdentityWithTx(tx, ExternalIdentityProviderTelegram, first.Id) })) require.NoError(t, DB.Transaction(func(tx *gorm.DB) error { return ClaimExternalIdentityWithTx(tx, ExternalIdentityProviderTelegram, "telegram-123", second.Id) })) } func TestClearTelegramBindingReleasesIdentityClaim(t *testing.T) { truncateTables(t) user := User{Username: "telegram-unbind", Password: "password", TelegramId: "telegram-unbind-id"} require.NoError(t, DB.Create(&user).Error) require.NoError(t, DB.Transaction(func(tx *gorm.DB) error { return ClaimExternalIdentityWithTx(tx, ExternalIdentityProviderTelegram, user.TelegramId, user.Id) })) require.NoError(t, user.ClearBinding(ExternalIdentityProviderTelegram)) assert.Empty(t, user.TelegramId) var count int64 require.NoError(t, DB.Model(&ExternalIdentityClaim{}).Where("user_id = ?", user.Id).Count(&count).Error) assert.Zero(t, count) } func TestInitializeExternalIdentityClaimsIsIdempotent(t *testing.T) { truncateTables(t) user := User{Username: "telegram-legacy", Password: "password", TelegramId: "telegram-legacy-id"} require.NoError(t, DB.Create(&user).Error) require.NoError(t, InitializeExternalIdentityClaims()) require.NoError(t, InitializeExternalIdentityClaims()) var claim ExternalIdentityClaim require.NoError(t, DB.Where("provider = ? AND subject = ?", ExternalIdentityProviderTelegram, user.TelegramId). First(&claim).Error) assert.Equal(t, user.Id, claim.UserId) } func TestInitializeExternalIdentityClaimsRejectsAmbiguousLegacyBindings(t *testing.T) { truncateTables(t) first := User{Username: "telegram-legacy-one", Password: "password", TelegramId: "duplicate-telegram-id", AffCode: "telegram-legacy-one"} second := User{Username: "telegram-legacy-two", Password: "password", TelegramId: "duplicate-telegram-id", AffCode: "telegram-legacy-two"} require.NoError(t, DB.Create(&first).Error) require.NoError(t, DB.Create(&second).Error) err := InitializeExternalIdentityClaims() assert.ErrorIs(t, err, ErrExternalIdentityAlreadyClaimed) var count int64 require.NoError(t, DB.Model(&ExternalIdentityClaim{}).Count(&count).Error) assert.Zero(t, count) }