Files
nezha_domains/model/server_owner_json_test.go
T
2026-05-25 10:17:34 +00:00

130 lines
4.0 KiB
Go

package model
import (
"encoding/json"
"testing"
)
// Server.MarshalJSON projects Server.UserID into a public owner field while
// keeping the raw UserID json-hidden. The lookup function is package-level
// and shared across tests; each subtest installs its own stub and restores
// the original to avoid leaking state.
func TestServerMarshalJSONOwnerProjection(t *testing.T) {
original := ServerOwnerLookup
t.Cleanup(func() { ServerOwnerLookup = original })
tests := []struct {
name string
uid uint64
lookup func(uid uint64) (ServerOwnerInfo, bool)
wantID uint64
wantHasName bool
wantName string
}{
{
// uid=0 is the legacy global agent secret pseudo-owner. The
// lookup deliberately returns ok=false so the frontend can
// render it as "Global Agent" instead of a real username.
name: "uid_zero_has_no_username",
uid: 0,
lookup: func(uint64) (ServerOwnerInfo, bool) {
return ServerOwnerInfo{}, false
},
wantID: 0,
wantHasName: false,
},
{
// Known user → username flows through to the wire so the
// admin frontend can show it without a separate /user fetch
// (which members cannot call anyway).
name: "known_user_has_username",
uid: 42,
lookup: func(uid uint64) (ServerOwnerInfo, bool) {
return ServerOwnerInfo{ID: uid, Username: "alice"}, true
},
wantID: 42,
wantHasName: true,
wantName: "alice",
},
{
// Deleted user → lookup returns ok=false. The wire still
// carries owner.id so the frontend can render an "Unknown
// user (#id)" placeholder; otherwise the row would silently
// appear ownerless and ops would lose the audit trail.
name: "deleted_user_keeps_id_without_username",
uid: 999,
lookup: func(uint64) (ServerOwnerInfo, bool) {
return ServerOwnerInfo{}, false
},
wantID: 999,
wantHasName: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ServerOwnerLookup = tc.lookup
s := &Server{Common: Common{ID: 7, UserID: tc.uid}, Name: "srv"}
raw, err := json.Marshal(s)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var got struct {
Owner *ServerOwnerInfo `json:"owner"`
// Owner must never appear as the raw uid via Common.UserID;
// the Common.UserID json tag is "-" and a regression that
// flips it to "user_id" would expose internal owner ids
// to the wire bypassing the lookup-controlled rendering.
UserID *uint64 `json:"user_id,omitempty"`
}
if err := json.Unmarshal(raw, &got); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if got.UserID != nil {
t.Fatalf("Common.UserID must not appear on the wire as user_id, got %d", *got.UserID)
}
if got.Owner == nil {
t.Fatalf("owner field must always be present, raw=%s", raw)
}
if got.Owner.ID != tc.wantID {
t.Fatalf("owner.id=%d, want %d", got.Owner.ID, tc.wantID)
}
if tc.wantHasName {
if got.Owner.Username != tc.wantName {
t.Fatalf("owner.username=%q, want %q", got.Owner.Username, tc.wantName)
}
} else if got.Owner.Username != "" {
t.Fatalf("owner.username must be omitted for uid=%d, got %q", tc.uid, got.Owner.Username)
}
})
}
}
// When no lookup is installed (tests / headless tools), MarshalJSON must
// still emit a minimal owner record so consumers do not crash on missing
// fields. Without this guard a future refactor could silently drop the
// owner key entirely whenever the hook is nil.
func TestServerMarshalJSONEmitsOwnerWithoutLookup(t *testing.T) {
original := ServerOwnerLookup
t.Cleanup(func() { ServerOwnerLookup = original })
ServerOwnerLookup = nil
raw, err := json.Marshal(&Server{Common: Common{ID: 1, UserID: 17}, Name: "srv"})
if err != nil {
t.Fatalf("marshal: %v", err)
}
var got struct {
Owner *ServerOwnerInfo `json:"owner"`
}
if err := json.Unmarshal(raw, &got); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if got.Owner == nil || got.Owner.ID != 17 || got.Owner.Username != "" {
t.Fatalf("expected bare owner record {id:17}, got %+v", got.Owner)
}
}