package dev.castarr.tv.server import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test class AttemptBudgetTest { private var clock = 0L private fun budget( windowMs: Long = 60_000L, maxFailures: Int = 5, maxTracked: Int = 64, ) = AttemptBudget(windowMs, maxFailures, maxTracked) { clock } /** * The 0.11.0 bug: the remote reconnects on every network hiccup, and * counting those successful handshakes threw the phone out after five. */ @Test fun `successful connections never cost budget`() { val budget = budget() repeat(50) { assertTrue(budget.allows("10.0.0.5")) budget.clear("10.0.0.5") } assertTrue(budget.allows("10.0.0.5")) } @Test fun `five failures in the window lock the address out`() { val budget = budget() repeat(5) { assertTrue(budget.allows("10.0.0.5")) budget.recordFailure("10.0.0.5") } assertFalse(budget.allows("10.0.0.5")) } @Test fun `a success after failures wipes the slate`() { val budget = budget() repeat(4) { budget.recordFailure("10.0.0.5") } budget.clear("10.0.0.5") repeat(4) { assertTrue(budget.allows("10.0.0.5")) budget.recordFailure("10.0.0.5") } assertTrue(budget.allows("10.0.0.5")) } @Test fun `failures expire once the window has passed`() { val budget = budget() repeat(5) { budget.recordFailure("10.0.0.5") } assertFalse(budget.allows("10.0.0.5")) clock += 60_001 assertTrue(budget.allows("10.0.0.5")) } @Test fun `the window slides, it does not reset in blocks`() { val budget = budget() repeat(4) { budget.recordFailure("10.0.0.5") clock += 20_000 } // Two of the four are older than a minute by now, so there is room. assertTrue(budget.allows("10.0.0.5")) } @Test fun `one address cannot spend another's budget`() { val budget = budget() repeat(5) { budget.recordFailure("10.0.0.9") } assertFalse(budget.allows("10.0.0.9")) assertTrue(budget.allows("10.0.0.5")) } /** An attacker cycling source addresses must not grow the map forever. */ @Test fun `tracked addresses stay bounded under a flood of fresh ones`() { val budget = budget(maxTracked = 8) repeat(500) { i -> budget.recordFailure("10.0.0.$i") clock += 10 } assertTrue(budget.trackedAddresses() <= 8) } @Test fun `eviction drops the stale addresses, not the active one`() { val budget = budget(maxTracked = 4) repeat(4) { i -> budget.recordFailure("10.0.1.$i") } clock += 60_001 repeat(5) { budget.recordFailure("10.0.0.5") } assertFalse(budget.allows("10.0.0.5")) assertEquals(1, budget.trackedAddresses()) } }