package user import ( "errors" "os" "path/filepath" "testing" ) func writeProfile(t *testing.T, content string) string { t.Helper() path := filepath.Join(t.TempDir(), "owner.yaml") if err := os.WriteFile(path, []byte(content), 0o600); err != nil { t.Fatalf("write owner profile: %v", err) } return path } func TestLoadOwnerProfile_Valid(t *testing.T) { t.Parallel() path := writeProfile(t, `id: owner displayName: 叶酱 defaultPersona: cyrene accounts: - platform: qq-obv11 accountId: "123456789" - platform: weixin-ilink accountId: wx_owner_abc notes: "主人" `) p, err := LoadOwnerProfile(path) if err != nil { t.Fatalf("LoadOwnerProfile error: %v", err) } if p.ID != "owner" || p.DisplayName != "叶酱" || p.DefaultPersona != "cyrene" { t.Fatalf("unexpected profile: %+v", p) } if !p.IsOwner("qq-obv11", "123456789") { t.Error("expected (qq-obv11,123456789) to be owner") } if !p.IsOwner("weixin-ilink", "wx_owner_abc") { t.Error("expected (weixin-ilink,wx_owner_abc) to be owner") } if p.IsOwner("qq-obv11", "999") { t.Error("non-owner account should not match") } if p.IsOwner("telegram", "123456789") { t.Error("wrong platform must not match") } } func TestLoadOwnerProfile_Validation(t *testing.T) { t.Parallel() tests := []struct { name string content string substr string }{ {name: "missing id", content: "displayName: 叶酱\naccounts:\n - platform: qq\n accountId: '1'\n", substr: "id"}, {name: "missing displayName", content: "id: owner\naccounts:\n - platform: qq\n accountId: '1'\n", substr: "displayName"}, {name: "account missing platform", content: "id: owner\ndisplayName: 叶酱\naccounts:\n - accountId: '1'\n", substr: "platform"}, {name: "account missing id", content: "id: owner\ndisplayName: 叶酱\naccounts:\n - platform: qq\n", substr: "accountId"}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() path := writeProfile(t, tt.content) _, err := LoadOwnerProfile(path) if err == nil { t.Fatal("expected validation error") } if !errors.Is(err, ErrRequiredField) { t.Fatalf("expected ErrRequiredField, got: %v", err) } if len(tt.substr) > 0 && !contains(err.Error(), tt.substr) { t.Fatalf("error %q should mention %q", err.Error(), tt.substr) } }) } } func TestLoadOwnerProfile_EmptyWhitelistDefaultsDeny(t *testing.T) { t.Parallel() path := writeProfile(t, "id: owner\ndisplayName: 叶酱\naccounts: []\n") p, err := LoadOwnerProfile(path) if err != nil { t.Fatalf("LoadOwnerProfile error: %v", err) } if p.IsOwner("qq-obv11", "123") { t.Error("empty whitelist must default to deny") } if len(p.AccountIdentifiers()) != 0 { t.Errorf("expected no account identifiers, got %v", p.AccountIdentifiers()) } } func TestOwnerProfile_AccountIdentifiers(t *testing.T) { t.Parallel() p := &OwnerProfile{ Accounts: []OwnerAccount{ {Platform: "qq", AccountID: "111"}, {Platform: "wx", AccountID: "222"}, {Platform: "qq", AccountID: "111"}, // duplicate }, } ids := p.AccountIdentifiers() if len(ids) != 2 { t.Fatalf("expected 2 unique identifiers, got %d: %v", len(ids), ids) } } func contains(s, sub string) bool { return len(sub) > 0 && len(s) >= len(sub) && (func() bool { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return true } } return false })() }