package e2e import ( "fmt" "net" "testing" "time" "github.com/iceBear67/redapricot/client" ) // expectClosedSoon fails unless conn is closed (or reset) within d. A player // that entered pending stays open until pendingTimeoutMs, so a fast EOF is // how we tell the limiter dropped the socket before match-side bookkeeping. func expectClosedSoon(t *testing.T, conn net.Conn, d time.Duration) { t.Helper() _ = conn.SetReadDeadline(time.Now().Add(d)) n, err := conn.Read(make([]byte, 16)) if err == nil { t.Fatalf("expected the hub to close the player, read %d bytes", n) } } // TestPlayerBurstDropsExtraHandshakes: more arrivals than playerBurst from the // same IP are closed after the handshake and never become pending. func TestPlayerBurstDropsExtraHandshakes(t *testing.T) { const psk = "e2e-rate-burst" port := freePort(t) hubAddr := fmt.Sprintf("127.0.0.1:%d", port) startHubCfg(t, port, psk, map[string]any{ "playerRatePerSec": 1, "playerBurst": 2, "maxPlayersPerIp": 64, }) dest := newMockDest(t, modeEcho) startClient(t, hubAddr, psk, 8, []client.Mapping{ {Pattern: "mc.local", Destination: dest.addr}, }) kept := make([]net.Conn, 0, 2) defer func() { for _, c := range kept { _ = c.Close() } }() for i := 0; i < 2; i++ { pc := dialPlayer(t, hubAddr, "mc.local") playerEcho(t, pc, []byte(fmt.Sprintf("ok-%d", i))) kept = append(kept, pc) } // Tokens spent, first two still held: extras must die well inside pendingTimeout. for i := 0; i < 2; i++ { extra := dialPlayer(t, hubAddr, "mc.local") expectClosedSoon(t, extra, 1500*time.Millisecond) _ = extra.Close() } // The admitted players are unaffected. playerEcho(t, kept[0], []byte("still-here")) } // TestMaxPlayersPerIpCapsConcurrentSockets: a second player from the same IP // is refused while the first is still open, and admitted again after it closes. func TestMaxPlayersPerIpCapsConcurrentSockets(t *testing.T) { const psk = "e2e-rate-conc" port := freePort(t) hubAddr := fmt.Sprintf("127.0.0.1:%d", port) startHubCfg(t, port, psk, map[string]any{ "playerRatePerSec": 0, "playerBurst": 16, "maxPlayersPerIp": 1, }) dest := newMockDest(t, modeEcho) startClient(t, hubAddr, psk, 4, []client.Mapping{ {Pattern: "mc.local", Destination: dest.addr}, }) first := dialPlayer(t, hubAddr, "mc.local") defer first.Close() playerEcho(t, first, []byte("first")) second := dialPlayer(t, hubAddr, "mc.local") expectClosedSoon(t, second, 1500*time.Millisecond) _ = second.Close() playerEcho(t, first, []byte("still-first")) _ = first.Close() // The closeHandler runs on the hub event loop; give it a beat to release. time.Sleep(200 * time.Millisecond) third := dialPlayer(t, hubAddr, "mc.local") defer third.Close() playerEcho(t, third, []byte("after-release")) } // TestUnmatchedHostConsumesRateBudget: a hostname scan is not a free flood. // Two unmatched handshakes spend the burst, so a later matching player is // also dropped. func TestUnmatchedHostConsumesRateBudget(t *testing.T) { const psk = "e2e-rate-scan" port := freePort(t) hubAddr := fmt.Sprintf("127.0.0.1:%d", port) startHubCfg(t, port, psk, map[string]any{ "playerRatePerSec": 1, "playerBurst": 2, "maxPlayersPerIp": 64, }) dest := newMockDest(t, modeEcho) startClient(t, hubAddr, psk, 4, []client.Mapping{ {Pattern: "mc.local", Destination: dest.addr}, }) for i := 0; i < 2; i++ { miss := dialPlayer(t, hubAddr, "no.such.host") expectClosedSoon(t, miss, 1500*time.Millisecond) _ = miss.Close() } matched := dialPlayer(t, hubAddr, "mc.local") expectClosedSoon(t, matched, 1500*time.Millisecond) _ = matched.Close() } // TestIntent17IgnoresPlayerLimiter: the control session and worker conns are // Intent 17, so a limiter tight enough to refuse a second player must not // prevent the client from connecting or taking over the first player. func TestIntent17IgnoresPlayerLimiter(t *testing.T) { const psk = "e2e-rate-intent17" port := freePort(t) hubAddr := fmt.Sprintf("127.0.0.1:%d", port) startHubCfg(t, port, psk, map[string]any{ "playerRatePerSec": 1, "playerBurst": 1, "maxPlayersPerIp": 1, }) dest := newMockDest(t, modeEcho) startClient(t, hubAddr, psk, 4, []client.Mapping{ {Pattern: "mc.local", Destination: dest.addr}, }) pc := dialPlayer(t, hubAddr, "mc.local") defer pc.Close() playerEcho(t, pc, []byte("intent17-ok")) } // TestPlayerLimiterOffSwitch: both knobs at 0 restore phase-1 behaviour — // many players from 127.0.0.1 all get through. func TestPlayerLimiterOffSwitch(t *testing.T) { const psk = "e2e-rate-off" const n = 6 port := freePort(t) hubAddr := fmt.Sprintf("127.0.0.1:%d", port) startHubCfg(t, port, psk, map[string]any{ "playerRatePerSec": 0, "maxPlayersPerIp": 0, }) dest := newMockDest(t, modeEcho) startClient(t, hubAddr, psk, n, []client.Mapping{ {Pattern: "mc.local", Destination: dest.addr}, }) conns := make([]net.Conn, 0, n) defer func() { for _, c := range conns { _ = c.Close() } }() for i := 0; i < n; i++ { pc := dialPlayer(t, hubAddr, "mc.local") playerEcho(t, pc, []byte(fmt.Sprintf("off-%d", i))) conns = append(conns, pc) } }