package subsonic import ( "context" "sync" "sync/atomic" "testing" "testing/synctest" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("authLimiter", func() { var ctx context.Context BeforeEach(func() { ctx = context.Background() }) acquire := func(l *authLimiter, key string) (*authSlot, bool) { GinkgoHelper() return l.acquire(ctx, key) } It("blocks a key after the configured number of failures", func() { l := newAuthLimiter(2, time.Minute) for range 2 { slot, ok := acquire(l, "k") Expect(ok).To(BeTrue()) slot.release(true) } _, ok := acquire(l, "k") Expect(ok).To(BeFalse()) }) It("never counts successful checks", func() { l := newAuthLimiter(2, time.Minute) for range 50 { slot, ok := acquire(l, "k") Expect(ok).To(BeTrue()) slot.release(false) } }) It("keeps keys independent", func() { l := newAuthLimiter(1, time.Minute) slot, _ := acquire(l, "a") slot.release(true) _, ok := acquire(l, "a") Expect(ok).To(BeFalse()) _, ok = acquire(l, "b") Expect(ok).To(BeTrue()) }) It("waits for an in-flight check instead of failing the request", func() { l := newAuthLimiter(1, time.Minute) held, ok := acquire(l, "k") Expect(ok).To(BeTrue()) waiting := make(chan bool, 1) go func() { slot, ok := l.acquire(ctx, "k") slot.release(false) waiting <- ok }() Consistently(waiting, 50*time.Millisecond).ShouldNot(Receive()) held.release(false) Eventually(waiting).Should(Receive(BeTrue())) }) It("stops waiting when the request is canceled", func() { l := newAuthLimiter(1, time.Minute) held, _ := acquire(l, "k") DeferCleanup(func() { held.release(false) }) canceled, cancel := context.WithCancel(context.Background()) cancel() _, ok := l.acquire(canceled, "k") Expect(ok).To(BeFalse()) }) It("does not let concurrent guesses overshoot the limit", func() { l := newAuthLimiter(5, time.Minute) hold := make(chan struct{}) var checks atomic.Int32 var wg sync.WaitGroup for range 50 { wg.Go(func() { slot, ok := l.acquire(ctx, "k") if !ok { return } checks.Add(1) <-hold slot.release(true) }) } Eventually(checks.Load).Should(Equal(int32(5))) Consistently(checks.Load, 100*time.Millisecond).Should(Equal(int32(5))) close(hold) wg.Wait() _, ok := acquire(l, "k") Expect(ok).To(BeFalse()) }) It("allows everything when the limit is disabled", func() { l := newAuthLimiter(0, time.Minute) for range 10 { slot, ok := acquire(l, "k") Expect(ok).To(BeTrue()) slot.release(true) } }) }) // testing/synctest's fake clock needs a *testing.T, which Ginkgo doesn't give. func TestAuthLimiterWindow(t *testing.T) { synctest.Test(t, func(t *testing.T) { g := NewWithT(t) ctx := context.Background() l := newAuthLimiter(1, 20*time.Second) for _, key := range []string{"a", "b"} { slot, ok := l.acquire(ctx, key) g.Expect(ok).To(BeTrue()) slot.release(true) } _, ok := l.acquire(ctx, "a") g.Expect(ok).To(BeFalse()) time.Sleep(20 * time.Second) slot, ok := l.acquire(ctx, "a") g.Expect(ok).To(BeTrue()) slot.release(false) g.Expect(l.keys).To(HaveLen(1), "expired keys must be dropped") }) }