package server import ( "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/require" "heckel.io/ntfy/v2/ban" ) // TestServer_BanFeed_WritesOffenderToFile is the end-to-end wiring test: a rejected request flows // through s.handle -> the error responder -> s.ban.Record, and once the offender's prefix // breaches, its ban line lands in the ban file (flushed on Close). func TestServer_BanFeed_WritesOffenderToFile(t *testing.T) { banFile := filepath.Join(t.TempDir(), "ban.log") conf := newTestConfig(t, "") conf.BanFile = banFile conf.BanWindow = time.Minute conf.BanThreshold = 1 // capacity 1: the 2nd rejection breaches conf.BanWeights = ban.Weights{"*": 1} // any 4xx counts one strike s := newTestServer(t, conf) require.NotNil(t, s.ban) // A delayed message with caching disabled is a deterministic 400 (errHTTPBadRequestDelayNoCache). // request() sends from RemoteAddr 9.9.9.9. reject := map[string]string{"Cache": "no", "In": "30 min"} for i := 0; i < 3; i++ { response := request(t, s, "PUT", "/mytopic", "", reject) require.Equal(t, 400, response.Code) } // Writes are async; Close flushes the buffer. The offender's prefix must be in the feed exactly // once (throttled to one line per window). s.ban.Close() data, err := os.ReadFile(banFile) require.NoError(t, err) lines := strings.Split(strings.TrimRight(string(data), "\n"), "\n") require.Len(t, lines, 1) require.Contains(t, lines[0], " 9.9.9.9 9.9.9.9/32 400 ") // } // TestServer_BanFeed_DisabledByDefault verifies the feature is off with no ban file: s.ban is // nil and the error path skips it (guarded), so a rejected request must not panic. func TestServer_BanFeed_DisabledByDefault(t *testing.T) { conf := newTestConfig(t, "") // no BanFile s := newTestServer(t, conf) require.Nil(t, s.ban) reject := map[string]string{"Cache": "no", "In": "30 min"} response := request(t, s, "PUT", "/mytopic", "", reject) // guarded callsite, no Record require.Equal(t, 400, response.Code) }