Files
ntfy/server/visitor_test.go
T
binwiederhier f16baadec4 Let a zero message, call or bandwidth limit admit nothing again
The limit service reads a zero cap as no cap, so a tier with no calls
(or no messages, or no attachment bandwidth on upload) became unlimited
where main's FixedLimiter(0) and bytes limiter admitted nothing. The
visitor now answers zero limits itself before asking the service, like
it already did for requests, emails and bandwidth downloads.
2026-10-25 21:11:36 +00:00

186 lines
7.3 KiB
Go

package server
import (
"net/netip"
"testing"
"time"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
)
func TestVisitor_UserStatsDeltaFlush(t *testing.T) {
forEachBackend(t, func(t *testing.T, databaseURL string) {
c := newTestConfigWithAuthFile(t, databaseURL)
c.AuthStatsQueueWriterInterval = 300 * time.Millisecond
s := newTestServer(t, c)
require.Nil(t, s.userManager.AddTier(&user.Tier{
Code: "test",
MessageLimit: 100,
}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
// Publish 3 messages; the visitor enqueues a stats delta after each publish
for i := 0; i < 3; i++ {
response := request(t, s, "PUT", "/mytopic", "test", map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, response.Code)
}
// After two flush intervals, the persisted count must be exactly 3: per-publish
// enqueues must be deltas, not absolute snapshots (snapshots would add up to 1+2+3=6)
time.Sleep(time.Second)
u, err := s.userManager.User("phil")
require.Nil(t, err)
require.Equal(t, int64(3), u.Stats.Messages)
})
}
func TestVisitor_SetUser_DoesNotChargeAnonymousUsage(t *testing.T) {
// A user who gets a tier takes over their IP-keyed visitor (e.g. the billing callback). What
// that visitor counted anonymously must not land on the account.
c := newTestConfigWithAuthFile(t, "")
c.AuthStatsQueueWriterInterval = 100 * time.Millisecond
s := newTestServer(t, c)
require.Nil(t, s.userManager.AddTier(&user.Tier{Code: "test", MessageLimit: 100}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
u, err := s.userManager.User("phil")
require.Nil(t, err)
v := s.visitor(netip.MustParseAddr("9.9.9.9"), nil)
for i := 0; i < 3; i++ {
require.True(t, v.MessageAllowed())
}
v.SetUser(u)
time.Sleep(500 * time.Millisecond)
u, err = s.userManager.User("phil")
require.Nil(t, err)
require.Equal(t, int64(0), u.Stats.Messages)
}
func TestVisitor_BandwidthRefund(t *testing.T) {
// A revert on the bandwidth limiter (what util.LimitReader and util.LimitWriter do when a
// later limiter rejects a chunk) hands the bytes back; it used to be a no-op
c := newTestConfig(t, "")
c.VisitorAttachmentDailyBandwidthLimit = 1000
s := newTestServer(t, c)
v := s.visitor(netip.MustParseAddr("9.9.9.9"), nil)
l := v.BandwidthLimiter()
require.True(t, l.AllowN(900))
require.False(t, l.AllowN(200))
require.True(t, l.AllowN(-500))
require.True(t, l.AllowN(200))
require.False(t, v.BandwidthAllowed(500))
require.True(t, v.BandwidthAllowed(400))
}
func TestVisitor_TierChangeKeepsBuckets(t *testing.T) {
// Moving between tiers re-resolves the limits on the next request, but the request bucket is
// not handed back full: what the user spent on the old tier stays spent
forEachBackend(t, func(t *testing.T, databaseURL string) {
c := newTestConfigWithAuthFile(t, databaseURL)
c.VisitorRequestLimitBurst = 3
c.VisitorRequestLimitReplenish = time.Hour
s := newTestServer(t, c)
require.Nil(t, s.userManager.AddTier(&user.Tier{Code: "small", MessageLimit: 10}))
require.Nil(t, s.userManager.AddTier(&user.Tier{Code: "large", MessageLimit: 10000})) // Burst 500
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeTier("phil", "small"))
for i := 0; i < 3; i++ {
response := request(t, s, "PUT", "/mytopic", "test", map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, response.Code)
}
response := request(t, s, "PUT", "/mytopic", "test", map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 429, response.Code)
require.Nil(t, s.userManager.ChangeTier("phil", "large"))
response = request(t, s, "PUT", "/mytopic", "test", map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 429, response.Code) // Still empty: the larger burst refills nothing
})
}
func TestVisitor_TierFieldChangeAppliesAtOnce(t *testing.T) {
// A changed limit on the user's existing tier applies on the next request, without the tier
// ID changing; it used to need a visitor rebuild
forEachBackend(t, func(t *testing.T, databaseURL string) {
c := newTestConfigWithAuthFile(t, databaseURL)
s := newTestServer(t, c)
require.Nil(t, s.userManager.AddTier(&user.Tier{Code: "test", MessageLimit: 2}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
for i := 0; i < 2; i++ {
response := request(t, s, "PUT", "/mytopic", "test", map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, response.Code)
}
response := request(t, s, "PUT", "/mytopic", "test", map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 429, response.Code)
tier, err := s.userManager.Tier("test")
require.Nil(t, err)
tier.MessageLimit = 3
require.Nil(t, s.userManager.UpdateTier(tier))
response = request(t, s, "PUT", "/mytopic", "test", map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, response.Code)
response = request(t, s, "PUT", "/mytopic", "test", map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 429, response.Code)
})
}
func TestVisitor_EmailDailyLimitCapsBelowBurst(t *testing.T) {
// The daily email limit (what the account page shows) is enforced, not only the bucket: with
// a burst of 16 and a refill every 12 hours, the day's limit is 2, and the third email is
// denied even though the bucket has tokens
c := newTestConfig(t, "")
c.VisitorEmailLimitBurst = 16
c.VisitorEmailLimitReplenish = 12 * time.Hour
s := newTestServer(t, c)
s.mailer = &testMailer{}
for i := 0; i < 2; i++ {
response := request(t, s, "PUT", "/mytopic", "test", map[string]string{
"Email": "test@email.com",
})
require.Equal(t, 200, response.Code)
}
response := request(t, s, "PUT", "/mytopic", "test", map[string]string{
"Email": "test@email.com",
})
require.Equal(t, 429, response.Code)
require.Equal(t, 42902, toHTTPError(t, response.Body.String()).Code)
}
func TestVisitor_ZeroLimitsAdmitNothing(t *testing.T) {
// A limit of zero admits nothing, as main's FixedLimiter(0) and bytes limiter did; the limit
// service reads a zero cap as "no cap", so the visitor must not hand it a zero (Codex review #3:
// a user downgraded to a tier without calls kept calling)
forEachBackend(t, func(t *testing.T, databaseURL string) {
c := newTestConfigWithAuthFile(t, databaseURL)
s := newTestServer(t, c)
require.Nil(t, s.userManager.AddTier(&user.Tier{Code: "zero", MessageLimit: 0, CallLimit: 0, AttachmentBandwidthLimit: 0}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeTier("phil", "zero"))
u, err := s.userManager.User("phil")
require.Nil(t, err)
v := s.visitor(netip.MustParseAddr("9.9.9.9"), u)
require.False(t, v.MessageAllowed())
require.False(t, v.CallAllowed())
require.False(t, v.BandwidthAllowed(1))
require.False(t, v.BandwidthLimiter().AllowN(1))
})
}