mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-28 14:17:10 +01:00
143 lines
3.2 KiB
Go
143 lines
3.2 KiB
Go
|
|
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")
|
||
|
|
})
|
||
|
|
}
|