fix(quota): 配额表单整体替换,留空即不限、0 即不给、负数和越界拒收,超配额的服务器仍可调小

This commit is contained in:
Lemon-miaow committed 2026-09-27 02:26:03 +08:00
1 parent ee4065b986
commit 7aa5034c1a
17 files changed
+346 -109

No files matched your search

+73
View File
@@ -553,6 +553,79 @@ func TestClaimServerQuotaAtomicGate(t *testing.T) {
}
}
// The quotas form replaces every cap at once: a nil field lifts that cap and 0 grants
// none of it. SetQuotas used to skip nil fields, so a cap once set could be moved but
// never removed, and the panel's emptied box changed nothing.
func TestSetQuotasReplacesEveryCap(t *testing.T) {
ctx := context.Background()
u := newUser(t, "user", "qrepl")
name := "qr-" + suffix(t)
if _, err := db.ExecContext(ctx,
`INSERT INTO servers (name, cached_cpu_milli, cached_memory_mb, cached_storage_mb) VALUES ($1, 100, 128, 1)`,
name); err != nil {
t.Fatalf("seed server: %v", err)
}
spec := api.ResourceSpec{CPUMilli: 100, MemoryMB: 128, StorageMB: 1}
n := func(v int) *int { return &v }
caps := func(q *api.QuotaView) string {
s := make([]string, 0, 4)
for _, v := range []*int{q.MaxServers, q.MaxCPUMilli, q.MaxMemoryMB, q.MaxStorageGB} {
if v == nil {
s = append(s, "-")
} else {
s = append(s, fmt.Sprint(*v))
}
}
return strings.Join(s, "/")
}
set := func(in api.QuotaInput, want string) {
t.Helper()
got, err := repo.SetQuotas(ctx, u.ID, in, "pgint-"+want)
if err != nil {
t.Fatalf("SetQuotas(%s): %v", want, err)
}
if caps(got) != want {
t.Fatalf("SetQuotas returned %s, want %s", caps(got), want)
}
read, err := repo.GetQuotas(ctx, u.ID)
if err != nil || caps(read) != want {
t.Fatalf("GetQuotas = %s, %v; want %s", caps(read), err, want)
}
var by string
if err := db.QueryRowContext(ctx, `SELECT updated_by FROM quotas WHERE user_id = $1`, u.ID).Scan(&by); err != nil || by != "pgint-"+want {
t.Fatalf("updated_by = %q, %v; want the latest writer", by, err)
}
}
gate := func(want bool) {
t.Helper()
if ok, err := repo.QuotaCheck(ctx, u.ID, "", spec); err != nil || ok != want {
t.Fatalf("QuotaCheck = %v, %v; want %v", ok, err, want)
}
}
set(api.QuotaInput{MaxServers: n(0), MaxCPUMilli: n(2000), MaxMemoryMB: n(4096), MaxStorageGB: n(20)}, "0/2000/4096/20")
gate(false)
if ok, err := repo.QuotaAvailable(ctx, u.ID); err != nil || ok {
t.Fatalf("QuotaAvailable at max_servers 0 = %v, %v; want false", ok, err)
}
if _, err := repo.ClaimServer(ctx, name, u.ID); !errors.Is(err, api.ErrQuotaExceeded) {
t.Fatalf("claim at max_servers 0 = %v, want ErrQuotaExceeded", err)
}
set(api.QuotaInput{MaxCPUMilli: n(2000), MaxStorageGB: n(20)}, "-/2000/-/20")
gate(true)
set(api.QuotaInput{MaxCPUMilli: n(0)}, "-/0/-/-")
gate(false)
set(api.QuotaInput{}, "-/-/-/-")
gate(true)
if ok, err := repo.QuotaAvailable(ctx, u.ID); err != nil || !ok {
t.Fatalf("QuotaAvailable with every cap lifted = %v, %v; want true", ok, err)
}
if claimed, err := repo.ClaimServer(ctx, name, u.ID); err != nil || !claimed {
t.Fatalf("claim with every cap lifted = %v, %v; want claimed", claimed, err)
}
}
// The fleet read needs every live server's claim state: the owner's id to tell
// the caller's own servers apart, the display name (email, else username), and
// the unclaimed rows too, since only those may be claimed. A soft-deleted row is