Smail/backend/services/externalemail/repository_test.go
smail 31ee54942a
Some checks are pending
Security Checks / security (push) Waiting to run
完善用户安全信息管理
2026-09-08 11:53:47 +08:00

168 lines
5.6 KiB
Go

package externalemail
import (
"path/filepath"
"testing"
"git.pcux.cn/xuans/Smail/models"
_ "modernc.org/sqlite"
"xorm.io/xorm"
)
func TestRepositoryBindsVerifiesAndScopesByUser(t *testing.T) {
engine, err := xorm.NewEngine("sqlite", filepath.Join(t.TempDir(), "external.db"))
if err != nil {
t.Fatal(err)
}
defer engine.Close()
if err := engine.Sync2(&models.ExternalEmail{}); err != nil {
t.Fatal(err)
}
vault := NewVaultWithKeys([]byte("01234567890123456789012345678901"), []byte("abcdefabcdefabcdefabcdefabcdefab"))
repo, err := NewRepository(engine, vault)
if err != nil {
t.Fatal(err)
}
item, err := repo.Bind(BindRequest{UserID: 1, Email: "Owner@Example.com", Type: models.ExternalEmailTypePrimary})
if err != nil {
t.Fatal(err)
}
if item.Status != models.ExternalEmailStatusPending || item.EmailCiphertext == "" {
t.Fatalf("unexpected binding: %+v", item)
}
pending, err := repo.FindPending(1, "OWNER@example.com", models.ExternalEmailTypePrimary)
if err != nil || pending.ID != item.ID {
t.Fatalf("pending binding = %+v, err = %v", pending, err)
}
if _, err := repo.Bind(BindRequest{UserID: 1, Email: "other@example.com", Type: models.ExternalEmailTypePrimary}); err != ErrPrimaryLimit {
t.Fatalf("primary limit error = %v", err)
}
if err := repo.Verify(2, item.ID); err != ErrBindingOwnership {
t.Fatalf("cross-user verify error = %v", err)
}
if err := repo.Verify(1, item.ID); err != nil {
t.Fatal(err)
}
if _, err := repo.FindPending(1, "owner@example.com", models.ExternalEmailTypePrimary); err != ErrBindingNotFound {
t.Fatalf("verified binding remained pending: %v", err)
}
stored, err := repo.List(1)
if err != nil || len(stored) != 1 {
t.Fatalf("list result = %v, %v", stored, err)
}
if stored[0].Status != models.ExternalEmailStatusVerified {
t.Fatalf("status = %s", stored[0].Status)
}
decrypted, err := repo.Decrypt(stored[0])
if err != nil || decrypted != "owner@example.com" {
t.Fatalf("decrypted email = %q, err = %v", decrypted, err)
}
}
func TestRepositoryRejectsInvalidEmailType(t *testing.T) {
engine, err := xorm.NewEngine("sqlite", filepath.Join(t.TempDir(), "external.db"))
if err != nil {
t.Fatal(err)
}
defer engine.Close()
vault := NewVaultWithKeys([]byte("01234567890123456789012345678901"), []byte("abcdefabcdefabcdefabcdefabcdefab"))
repo, err := NewRepository(engine, vault)
if err != nil {
t.Fatal(err)
}
if _, err := repo.Bind(BindRequest{UserID: 1, Email: "owner@example.com", Type: "unknown"}); err != ErrInvalidBinding {
t.Fatalf("error = %v, want invalid binding", err)
}
}
func TestRepositoryAbandonsOnlyOwnedPendingBinding(t *testing.T) {
engine, err := xorm.NewEngine("sqlite", filepath.Join(t.TempDir(), "external.db"))
if err != nil {
t.Fatal(err)
}
defer engine.Close()
if err := engine.Sync2(&models.ExternalEmail{}); err != nil {
t.Fatal(err)
}
vault := NewVaultWithKeys([]byte("01234567890123456789012345678901"), []byte("abcdefabcdefabcdefabcdefabcdefab"))
repo, err := NewRepository(engine, vault)
if err != nil {
t.Fatal(err)
}
item, err := repo.Bind(BindRequest{UserID: 1, Email: "pending@example.com", Type: models.ExternalEmailTypePrimary})
if err != nil {
t.Fatal(err)
}
if err := repo.AbandonPending(2, item.ID); err != ErrBindingOwnership {
t.Fatalf("cross-user abandon error = %v", err)
}
if err := repo.AbandonPending(1, item.ID); err != nil {
t.Fatal(err)
}
if count, err := engine.ID(item.ID).Count(&models.ExternalEmail{}); err != nil || count != 0 {
t.Fatalf("pending binding remains: count=%d err=%v", count, err)
}
verified, err := repo.Bind(BindRequest{UserID: 1, Email: "verified@example.com", Type: models.ExternalEmailTypePrimary})
if err != nil {
t.Fatal(err)
}
if err := repo.Verify(1, verified.ID); err != nil {
t.Fatal(err)
}
if err := repo.AbandonPending(1, verified.ID); err != ErrBindingOwnership {
t.Fatalf("verified binding was abandoned: %v", err)
}
}
func TestRepositoryReplacesExternalEmailOnlyAfterVerification(t *testing.T) {
engine, err := xorm.NewEngine("sqlite", filepath.Join(t.TempDir(), "external.db"))
if err != nil {
t.Fatal(err)
}
defer engine.Close()
if err := engine.Sync2(&models.ExternalEmail{}); err != nil {
t.Fatal(err)
}
vault := NewVaultWithKeys([]byte("01234567890123456789012345678901"), []byte("abcdefabcdefabcdefabcdefabcdefab"))
repo, err := NewRepository(engine, vault)
if err != nil {
t.Fatal(err)
}
oldBinding, err := repo.Bind(BindRequest{UserID: 1, Email: "old@example.com", Type: models.ExternalEmailTypePrimary})
if err != nil {
t.Fatal(err)
}
if err := repo.Verify(1, oldBinding.ID); err != nil {
t.Fatal(err)
}
pending, err := repo.BeginReplacement(BindRequest{UserID: 1, Email: "new@example.com", Type: models.ExternalEmailTypePrimary})
if err != nil {
t.Fatal(err)
}
before, err := repo.FindVerifiedPrimaryByUser(1)
if err != nil || before.ID != oldBinding.ID {
t.Fatalf("old binding was not retained before verification: %+v, %v", before, err)
}
if err := repo.VerifyReplacement(2, pending.ID); err != ErrBindingOwnership {
t.Fatalf("cross-user replacement error = %v", err)
}
if err := repo.VerifyReplacement(1, pending.ID); err != nil {
t.Fatal(err)
}
after, err := repo.FindVerifiedPrimaryByUser(1)
if err != nil || after.ID != pending.ID {
t.Fatalf("new binding was not activated: %+v, %v", after, err)
}
items, err := repo.List(1)
if err != nil {
t.Fatal(err)
}
statuses := map[int]string{}
for _, item := range items {
statuses[item.ID] = item.Status
}
if statuses[oldBinding.ID] != models.ExternalEmailStatusDisabled || statuses[pending.ID] != models.ExternalEmailStatusVerified {
t.Fatalf("unexpected replacement statuses: %+v", statuses)
}
}