package main import ( "path/filepath" "testing" "time" ) type clock struct{ now time.Time } func (c *clock) Now() time.Time { return c.now } func testLimiter(t *testing.T, cfg Config, at string) (*Limiter, *clock) { t.Helper() moment, err := time.Parse(time.RFC3339, at) if err != nil { t.Fatal(err) } cfg.Timezone = "UTC" c := &clock{now: moment} limiter, err := NewLimiter(cfg, "", c.Now) if err != nil { t.Fatal(err) } return limiter, c } func TestInWindowTable(t *testing.T) { at := func(text string) time.Time { moment, _ := time.Parse("15:04", text) return moment } cases := []struct { from, to, now string want bool }{ {"", "", "12:00", true}, {"09:00", "17:00", "09:00", true}, {"09:00", "17:00", "16:59", true}, {"09:00", "17:00", "17:00", false}, {"09:00", "17:00", "08:59", false}, {"22:00", "06:00", "23:30", true}, {"22:00", "06:00", "05:59", true}, {"22:00", "06:00", "06:00", false}, {"22:00", "06:00", "12:00", false}, {"10:00", "10:00", "03:00", true}, } for _, item := range cases { if got := InWindow(item.from, item.to, at(item.now)); got != item.want { t.Errorf("%s-%s at %s: got %v", item.from, item.to, item.now, got) } } } func TestAcquireConcurrencyCapAndServerCannotWiden(t *testing.T) { cfg := DefaultConfig() cfg.MaxConcurrent = 2 limiter, _ := testLimiter(t, cfg, "2026-10-15T12:00:00Z") limiter.ApplyServer(&ServerLimits{MaxConcurrentPerNode: 8}, &ServerSettings{MaxConcurrent: 8}) if limiter.Acquire() != "" || limiter.Acquire() != "" { t.Fatal("two slots should be free") } if got := limiter.Acquire(); got != RefuseConcurrency { t.Fatalf("third acquire: %q", got) } limiter.Release() if got := limiter.Acquire(); got != "" { t.Fatalf("after release: %q", got) } } func TestServerCanNarrowConcurrency(t *testing.T) { cfg := DefaultConfig() cfg.MaxConcurrent = 4 limiter, _ := testLimiter(t, cfg, "2026-10-15T12:00:00Z") limiter.ApplyServer(&ServerLimits{MaxConcurrentPerNode: 2}, &ServerSettings{MaxConcurrent: 1}) if limiter.Acquire() != "" { t.Fatal("first slot") } if got := limiter.Acquire(); got != RefuseConcurrency { t.Fatalf("got %q", got) } } func TestBandwidthCapLocalAndDayRollover(t *testing.T) { cfg := DefaultConfig() cfg.BandwidthMBDay = 1 limiter, c := testLimiter(t, cfg, "2026-10-15T23:00:00Z") limiter.ApplyServer(&ServerLimits{MaxDonorMBPerDay: 20480}, &ServerSettings{BandwidthMBDay: 9999}) if limiter.Acquire() != "" { t.Fatal("fresh day must allow") } limiter.Release() limiter.AddBytes(mebibyte - 10) if got := limiter.Remaining(); got != 10 { t.Fatalf("remaining %d", got) } limiter.AddBytes(10) if got := limiter.Acquire(); got != RefuseBandwidth { t.Fatalf("got %q", got) } c.now = c.now.Add(2 * time.Hour) if got := limiter.Acquire(); got != "" { t.Fatalf("next day: %q", got) } } func TestServerCanNarrowBandwidth(t *testing.T) { cfg := DefaultConfig() cfg.BandwidthMBDay = 500 limiter, _ := testLimiter(t, cfg, "2026-10-15T12:00:00Z") limiter.ApplyServer(nil, &ServerSettings{BandwidthMBDay: 2}) if got := limiter.Remaining(); got != 2*mebibyte { t.Fatalf("remaining %d", got) } } func TestActiveHoursLocalAndServer(t *testing.T) { cfg := DefaultConfig() cfg.ActiveFrom, cfg.ActiveTo = "22:00", "06:00" limiter, c := testLimiter(t, cfg, "2026-10-15T12:00:00Z") if got := limiter.Acquire(); got != RefuseHours { t.Fatalf("noon: %q", got) } c.now = c.now.Add(11 * time.Hour) if got := limiter.Acquire(); got != "" { t.Fatalf("23:00: %q", got) } limiter.Release() limiter.ApplyServer(nil, &ServerSettings{ActiveFrom: "01:00", ActiveTo: "02:00", Timezone: "UTC"}) if got := limiter.Acquire(); got != RefuseHours { t.Fatalf("server window: %q", got) } } func TestUsagePersistsAcrossRestart(t *testing.T) { path := filepath.Join(t.TempDir(), "usage.json") cfg := DefaultConfig() cfg.Timezone = "UTC" moment := time.Date(2026, 10, 15, 12, 0, 0, 0, time.UTC) c := &clock{now: moment} first, err := NewLimiter(cfg, path, c.Now) if err != nil { t.Fatal(err) } first.AddBytes(12345) first.Flush() second, err := NewLimiter(cfg, path, c.Now) if err != nil { t.Fatal(err) } if _, used, _ := second.Usage(); used != 12345 { t.Fatalf("used %d", used) } c.now = moment.Add(24 * time.Hour) if _, used, _ := second.Usage(); used != 0 { t.Fatalf("next day used %d", used) } if got := ReadUsage(path, time.UTC, moment); got != 12345 { t.Fatalf("read usage %d", got) } }