152 lines
4.8 KiB
Go
152 lines
4.8 KiB
Go
package user
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// TestSaveOwnerProfile_RoundTrip verifies a profile loaded from disk survives a
|
|
// Save → Load cycle without dropping curated fields (notes, identityFile, ...) or
|
|
// the account whitelist. It also exercises the atomic-write + parent-dir-create
|
|
// path (writing under a not-yet-existing subdirectory).
|
|
func TestSaveOwnerProfile_RoundTrip(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
src := writeProfile(t, `id: owner
|
|
displayName: 叶酱
|
|
defaultPersona: cyrene
|
|
identityFile: configs/users/owner_identity.md
|
|
accounts:
|
|
- platform: kook
|
|
accountId: "329284282"
|
|
notes: "主人。陪伴对象。"
|
|
`)
|
|
orig, err := LoadOwnerProfile(src)
|
|
if err != nil {
|
|
t.Fatalf("load source: %v", err)
|
|
}
|
|
|
|
dst := filepath.Join(t.TempDir(), "nested", "deep", "owner.yaml")
|
|
if err := SaveOwnerProfile(dst, orig); err != nil {
|
|
t.Fatalf("SaveOwnerProfile: %v", err)
|
|
}
|
|
info, err := os.Stat(dst)
|
|
if err != nil {
|
|
t.Fatalf("stat saved profile: %v", err)
|
|
}
|
|
if perm := info.Mode().Perm(); perm != 0o600 {
|
|
t.Errorf("saved file mode = %o, want 0600", perm)
|
|
}
|
|
|
|
got, err := LoadOwnerProfile(dst)
|
|
if err != nil {
|
|
t.Fatalf("load saved: %v", err)
|
|
}
|
|
if got.ID != "owner" || got.DisplayName != "叶酱" || got.DefaultPersona != "cyrene" {
|
|
t.Fatalf("scalar fields = %+v, want owner/叶酱/cyrene", got)
|
|
}
|
|
if got.IdentityFile != "configs/users/owner_identity.md" {
|
|
t.Errorf("identityFile = %q, want preserved", got.IdentityFile)
|
|
}
|
|
if got.Notes != "主人。陪伴对象。" {
|
|
t.Errorf("notes = %q, want preserved", got.Notes)
|
|
}
|
|
if !got.IsOwner("kook", "329284282") {
|
|
t.Error("saved profile should still recognize the kook owner account")
|
|
}
|
|
}
|
|
|
|
// TestSaveOwnerProfile_NilProfile errors instead of panicking.
|
|
func TestSaveOwnerProfile_NilProfile(t *testing.T) {
|
|
t.Parallel()
|
|
if err := SaveOwnerProfile(filepath.Join(t.TempDir(), "o.yaml"), nil); err == nil {
|
|
t.Fatal("expected an error for a nil profile")
|
|
}
|
|
}
|
|
|
|
// TestRegisterOwnerAccount_AppendAndDedupe verifies that a new account is appended
|
|
// to the whitelist and recognized, a duplicate registration is a no-op, and an
|
|
// already-present account is never duplicated.
|
|
func TestRegisterOwnerAccount_AppendAndDedupe(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
path := writeProfile(t, `id: owner
|
|
displayName: 叶酱
|
|
defaultPersona: cyrene
|
|
accounts:
|
|
- platform: kook
|
|
accountId: "329284282"
|
|
notes: "主人"
|
|
`)
|
|
|
|
if err := RegisterOwnerAccount(path, "weixin-ilink", "wx1"); err != nil {
|
|
t.Fatalf("register new account: %v", err)
|
|
}
|
|
p, err := LoadOwnerProfile(path)
|
|
if err != nil {
|
|
t.Fatalf("reload: %v", err)
|
|
}
|
|
if !p.IsOwner("weixin-ilink", "wx1") {
|
|
t.Error("newly registered (weixin-ilink, wx1) must be recognized as owner")
|
|
}
|
|
if !p.IsOwner("kook", "329284282") {
|
|
t.Error("existing (kook, 329284282) must still be recognized")
|
|
}
|
|
if got := len(p.Accounts); got != 2 {
|
|
t.Fatalf("accounts = %d, want 2 (kook + weixin-ilink)", got)
|
|
}
|
|
|
|
// Duplicate registration is a no-op.
|
|
if err := RegisterOwnerAccount(path, "weixin-ilink", "wx1"); err != nil {
|
|
t.Fatalf("re-register same account: %v", err)
|
|
}
|
|
p, _ = LoadOwnerProfile(path)
|
|
if got := len(p.Accounts); got != 2 {
|
|
t.Fatalf("accounts after duplicate = %d, want still 2", got)
|
|
}
|
|
|
|
// Re-registering an existing kook account is also a no-op.
|
|
if err := RegisterOwnerAccount(path, "kook", "329284282"); err != nil {
|
|
t.Fatalf("re-register existing kook: %v", err)
|
|
}
|
|
p, _ = LoadOwnerProfile(path)
|
|
if got := len(p.Accounts); got != 2 {
|
|
t.Fatalf("accounts after kook dedupe = %d, want still 2", got)
|
|
}
|
|
}
|
|
|
|
// TestRegisterOwnerAccount_ValidatesInput rejects empty platform / account id.
|
|
func TestRegisterOwnerAccount_ValidatesInput(t *testing.T) {
|
|
t.Parallel()
|
|
path := writeProfile(t, "id: owner\ndisplayName: 叶酱\naccounts: []\n")
|
|
if err := RegisterOwnerAccount(path, "", "wx1"); err == nil {
|
|
t.Error("empty platform must error")
|
|
}
|
|
if err := RegisterOwnerAccount(path, "weixin-ilink", ""); err == nil {
|
|
t.Error("empty account id must error")
|
|
}
|
|
}
|
|
|
|
// TestRegisterAccount_HotEffect verifies the in-memory mutation the channel bridge
|
|
// relies on: after RegisterAccount on a live profile, IsOwner sees it immediately
|
|
// and a duplicate is a no-op. This is the hot-reload path (no restart).
|
|
func TestRegisterAccount_HotEffect(t *testing.T) {
|
|
t.Parallel()
|
|
p := &OwnerProfile{ID: "owner", DisplayName: "叶酱",
|
|
Accounts: []OwnerAccount{{Platform: "kook", AccountID: "329284282"}}}
|
|
|
|
p.RegisterAccount("weixin-ilink", "wx1")
|
|
if !p.IsOwner("weixin-ilink", "wx1") {
|
|
t.Error("hot-registered (weixin-ilink, wx1) must be recognized immediately")
|
|
}
|
|
if len(p.Accounts) != 2 {
|
|
t.Fatalf("accounts = %d, want 2", len(p.Accounts))
|
|
}
|
|
|
|
p.RegisterAccount("weixin-ilink", "wx1") // duplicate
|
|
if len(p.Accounts) != 2 {
|
|
t.Fatalf("accounts after duplicate = %d, want still 2", len(p.Accounts))
|
|
}
|
|
}
|