fix(panel): 服务器列表的归属与可认领改由后端按账号判定,无邮箱管理员也能认出自己的服务器,所有者查询失败时显示未知且不给认领

This commit is contained in:
Lemon-miaow committed 2026-09-25 12:36:38 +08:00
1 parent 06d5e652c6
commit bb9c2168df
14 files changed
+323 -70

No files matched your search

+58 -23
View File
@@ -33,10 +33,10 @@ type fakeRepo struct {
// the account_links-bridged web view of the same data.
allowUUID map[string]map[string]bool
mine map[string][]MyServerView
// owners mirrors the ServerOwners join (name -> owner display identity); only
// claimed servers appear. ownersErr forces the lookup to fail so a test can
// prove the fleet read degrades to owner-less rows rather than 500ing.
owners map[string]string
// owners mirrors the ServerOwners join (name -> claim state); a live unclaimed
// server appears with an empty OwnerID. ownersErr forces the lookup to fail so
// a test can prove the fleet read degrades rather than 500ing.
owners map[string]ServerOwnership
ownersErr error
claimOK map[string]bool // name -> claim succeeds; absent name -> ErrNotFound
// claimQuotaRefuse simulates ClaimServer's atomic quota gate (audit #4)
@@ -255,7 +255,7 @@ func newFakeRepo() *fakeRepo {
linked: map[string]bool{}, quota: map[string]bool{},
allowlist: map[string]map[string]bool{}, allowUUID: map[string]map[string]bool{},
mine: map[string][]MyServerView{},
owners: map[string]string{},
owners: map[string]ServerOwnership{},
claimOK: map[string]bool{}, claimQuotaRefuse: map[string]bool{},
serverResources: map[string]ResourceSpec{}, resourceUpdates: map[string]ResourceSpec{},
seeded: map[string]bool{}, aliases: map[string]string{},
@@ -750,7 +750,7 @@ func (f *fakeRepo) MyServers(_ context.Context, u string) ([]MyServerView, error
// into the slice it gets.
return append([]MyServerView(nil), f.mine[u]...), nil
}
func (f *fakeRepo) ServerOwners(_ context.Context) (map[string]string, error) {
func (f *fakeRepo) ServerOwners(_ context.Context) (map[string]ServerOwnership, error) {
if f.ownersErr != nil {
return nil, f.ownersErr
}
@@ -1971,11 +1971,13 @@ func TestFleetAdminRead(t *testing.T) {
})
// fleetRow mirrors the on-the-wire fleetServerView: the lifecycle fields plus
// the presentational owner join. A server absent from ServerOwners (unclaimed)
// or a failed lookup must serialize owner as "" (omitempty drops it).
// the ownership join.
type fleetRow struct {
Name string `json:"name"`
Owner string `json:"owner"`
Name string `json:"name"`
Owner string `json:"owner"`
Owned bool `json:"owned"`
Claimable bool `json:"claimable"`
OwnerUnknown bool `json:"ownerUnknown"`
}
adminAPI := func(repo *fakeRepo) *API {
api := newTestAPI(repo, cl)
@@ -2042,33 +2044,66 @@ func TestFleetAdminRead(t *testing.T) {
}
})
t.Run("owner merges for claimed, absent for unclaimed", func(t *testing.T) {
t.Run("ownership comes from the account id", func(t *testing.T) {
repo := newFakeRepo()
// Only "survival" is claimed; "creative"/"skyblock" stay unowned.
repo.owners["survival"] = "[email protected]"
byName := map[string]string{}
// The caller (a1) owns "survival" but has no email, so its display is the
// username; "creative" belongs to someone else; "skyblock" is unclaimed.
repo.owners["survival"] = ServerOwnership{OwnerID: "a1", Owner: "a1-username"}
repo.owners["creative"] = ServerOwnership{OwnerID: "u2", Owner: "[email protected]"}
repo.owners["skyblock"] = ServerOwnership{}
byName := map[string]fleetRow{}
for _, r := range readFleet(t, adminAPI(repo)) {
byName[r.Name] = r.Owner
byName[r.Name] = r
}
if byName["survival"] != "[email protected]" {
t.Fatalf("survival owner = %q, want [email protected]", byName["survival"])
want := map[string]fleetRow{
"survival": {Name: "survival", Owner: "a1-username", Owned: true},
"creative": {Name: "creative", Owner: "[email protected]"},
"skyblock": {Name: "skyblock", Claimable: true},
}
if byName["creative"] != "" {
t.Fatalf("creative owner = %q, want empty (unclaimed)", byName["creative"])
for name, w := range want {
if byName[name] != w {
t.Errorf("%s = %+v, want %+v", name, byName[name], w)
}
}
})
t.Run("owner lookup failure degrades to owner-less rows", func(t *testing.T) {
t.Run("a server without a business row cannot be claimed", func(t *testing.T) {
// A CRD the servers table does not know (or a soft-deleted row) answers a
// claim with 404, so the row must not offer one.
rows := readFleet(t, adminAPI(newFakeRepo()))
for _, r := range rows {
if r.Claimable || r.Owned || r.OwnerUnknown {
t.Errorf("%s = %+v, want no claim state (no business row)", r.Name, r)
}
}
})
t.Run("system services are never claimable", func(t *testing.T) {
sysCl := newFakeCluster()
sysCl.list = []ServerInfo{{Name: "lobby", Phase: "Running", Ready: true}}
repo := newFakeRepo()
repo.owners["survival"] = "[email protected]" // would merge, but the lookup errors
repo.owners["lobby"] = ServerOwnership{}
api := newTestAPI(repo, sysCl)
api.External = staticExternal{p: &Principal{UserID: "a1", Email: "[email protected]",
Role: "admin", ViaAdminAccess: true}}
rows := readFleet(t, api)
if len(rows) != 1 || rows[0].Claimable {
t.Fatalf("rows = %+v, want lobby present and not claimable", rows)
}
})
t.Run("owner lookup failure marks ownership unknown", func(t *testing.T) {
repo := newFakeRepo()
repo.owners["survival"] = ServerOwnership{OwnerID: "u2", Owner: "[email protected]"} // would merge, but the lookup errors
repo.owners["skyblock"] = ServerOwnership{}
repo.ownersErr = fmt.Errorf("postgres unreachable")
rows := readFleet(t, adminAPI(repo)) // must still be 200, not 500
if len(rows) != 3 {
t.Fatalf("servers = %d, want 3 (a Postgres blip must not drop the fleet)", len(rows))
}
for _, r := range rows {
if r.Owner != "" {
t.Fatalf("%s owner = %q, want empty (owner lookup failed → degrade)", r.Name, r.Owner)
if r.Owner != "" || r.Claimable || r.Owned || !r.OwnerUnknown {
t.Fatalf("%s = %+v, want owner unknown and nothing to claim", r.Name, r)
}
}
})