@@ -2004,3 +2004,253 @@ func Test_SSE_Bridge_PanicsWithoutSubscriber(t *testing.T) {
20042004 })
20052005 })
20062006}
2007+
2008+ // ──────────────────────────────────────────────────────────────────────────────
2009+ // Coverage boosters — targeted tests for previously-uncovered branches.
2010+ // ──────────────────────────────────────────────────────────────────────────────
2011+
2012+ func Test_SSE_Publish_DropsDuringDrain (t * testing.T ) {
2013+ t .Parallel ()
2014+
2015+ _ , hub := NewWithHub ()
2016+
2017+ // Flip the drain flag and Publish — event must be counted as dropped,
2018+ // not published. Exercises the early-return branch in Publish.
2019+ hub .draining .Store (true )
2020+ hub .Publish (Event {Type : "x" , Topics : []string {"t" }, Data : "d" })
2021+
2022+ stats := hub .Stats ()
2023+ require .Equal (t , int64 (1 ), stats .EventsDropped )
2024+ require .Equal (t , int64 (0 ), stats .EventsPublished )
2025+
2026+ hub .draining .Store (false )
2027+ ctx , cancel := context .WithTimeout (context .Background (), time .Second )
2028+ defer cancel ()
2029+ require .NoError (t , hub .Shutdown (ctx ))
2030+ }
2031+
2032+ func Test_SSE_Publish_StampsCreatedAtWhenTTLSet (t * testing.T ) {
2033+ t .Parallel ()
2034+
2035+ _ , hub := NewWithHub ()
2036+ defer func () {
2037+ ctx , cancel := context .WithTimeout (context .Background (), time .Second )
2038+ defer cancel ()
2039+ require .NoError (t , hub .Shutdown (ctx ))
2040+ }()
2041+
2042+ // TTL > 0 with zero CreatedAt — Publish must stamp CreatedAt so
2043+ // routeEvent can compute age correctly.
2044+ before := time .Now ()
2045+ hub .Publish (Event {
2046+ Type : "x" ,
2047+ Topics : []string {"t" },
2048+ Data : "d" ,
2049+ TTL : time .Second ,
2050+ })
2051+ time .Sleep (30 * time .Millisecond )
2052+
2053+ // No direct getter for the enqueued event; just assert the
2054+ // corresponding counter to ensure we hit the enqueue branch.
2055+ stats := hub .Stats ()
2056+ require .Equal (t , int64 (1 ), stats .EventsPublished )
2057+ require .Less (t , time .Since (before ), time .Second )
2058+ }
2059+
2060+ func Test_SSE_WriteRetry_SkipsNonPositive (t * testing.T ) {
2061+ t .Parallel ()
2062+
2063+ var buf bytes.Buffer
2064+ require .NoError (t , writeRetry (& buf , 0 ))
2065+ require .NoError (t , writeRetry (& buf , - 42 ))
2066+ require .Empty (t , buf .String (), "non-positive ms must not emit a retry: directive" )
2067+
2068+ require .NoError (t , writeRetry (& buf , 1500 ))
2069+ require .Contains (t , buf .String (), "retry: 1500\n " )
2070+ }
2071+
2072+ func Test_SSE_TrackEventType_EmptyDefaultsToMessage (t * testing.T ) {
2073+ t .Parallel ()
2074+
2075+ m := & hubMetrics {eventsByType : make (map [string ]* atomic.Int64 )}
2076+ m .trackEventType ("" )
2077+ m .trackEventType ("" )
2078+ m .trackEventType ("custom" )
2079+
2080+ snap := m .snapshotEventsByType ()
2081+ require .Equal (t , int64 (2 ), snap ["message" ], "empty event type falls back to \" message\" " )
2082+ require .Equal (t , int64 (1 ), snap ["custom" ])
2083+ }
2084+
2085+ func Test_SSE_MatchGroupConns_EmptyGroupIsNoOp (t * testing.T ) {
2086+ t .Parallel ()
2087+
2088+ _ , hub := NewWithHub ()
2089+ defer func () {
2090+ ctx , cancel := context .WithTimeout (context .Background (), time .Second )
2091+ defer cancel ()
2092+ require .NoError (t , hub .Shutdown (ctx ))
2093+ }()
2094+
2095+ // With no Group set on the event, matchGroupConns must short-circuit
2096+ // without scanning connections — the early-return branch.
2097+ seen := make (map [string ]struct {})
2098+ hub .mu .RLock ()
2099+ hub .matchGroupConns (& Event {Type : "x" , Topics : []string {"t" }}, seen )
2100+ hub .mu .RUnlock ()
2101+ require .Empty (t , seen )
2102+ }
2103+
2104+ func Test_SSE_WatchLifetime_NoOpWhenDisabled (t * testing.T ) {
2105+ t .Parallel ()
2106+
2107+ // MaxLifetime <= 0 must leave watchLifetime as a no-op (no goroutine
2108+ // spawned, no eventual Close on the connection).
2109+ _ , hub := NewWithHub (Config {MaxLifetime : - 1 })
2110+ defer func () {
2111+ ctx , cancel := context .WithTimeout (context .Background (), time .Second )
2112+ defer cancel ()
2113+ require .NoError (t , hub .Shutdown (ctx ))
2114+ }()
2115+
2116+ conn := newConnection ("c1" , []string {"t" }, 8 , 100 * time .Millisecond )
2117+ hub .watchLifetime (conn )
2118+
2119+ // If watchLifetime spawned a goroutine it would close `conn.done`
2120+ // eventually (with MaxLifetime<=0 it must NOT). Allow plenty of
2121+ // scheduler time then assert conn is still alive.
2122+ time .Sleep (50 * time .Millisecond )
2123+ require .False (t , conn .IsClosed (), "watchLifetime must not close the conn when MaxLifetime<=0" )
2124+ }
2125+
2126+ func Test_SSE_ReplayEvents_NoReplayerOrEmptyLastEventID (t * testing.T ) {
2127+ t .Parallel ()
2128+
2129+ // nil Replayer OR empty Last-Event-ID — both branches return nil
2130+ // without touching the writer.
2131+ hub := & Hub {cfg : Config {Replayer : nil }}
2132+ conn := newConnection ("c1" , []string {"t" }, 8 , 100 * time .Millisecond )
2133+ var buf bytes.Buffer
2134+ require .NoError (t , hub .replayEvents (bufio .NewWriter (& buf ), conn , "some-id" ))
2135+ require .Empty (t , buf .String ())
2136+
2137+ hub2 := & Hub {cfg : Config {Replayer : & testReplayer {}}}
2138+ require .NoError (t , hub2 .replayEvents (bufio .NewWriter (& buf ), conn , "" ))
2139+ require .Empty (t , buf .String ())
2140+ }
2141+
2142+ // failingWriter writes `limit` bytes successfully then returns errWrite on
2143+ // subsequent writes. Used to hit the error branches in initStream, replayEvents,
2144+ // sendConnectedEvent, and writeLoop without spinning up a real TCP listener.
2145+ type failingWriter struct {
2146+ err error
2147+ written int
2148+ limit int
2149+ }
2150+
2151+ func (fw * failingWriter ) Write (p []byte ) (int , error ) {
2152+ if fw .err != nil && fw .written >= fw .limit {
2153+ return 0 , fw .err
2154+ }
2155+ fw .written += len (p )
2156+ return len (p ), nil
2157+ }
2158+
2159+ func Test_SSE_InitStream_PropagatesWriteErrors (t * testing.T ) {
2160+ t .Parallel ()
2161+
2162+ // Fail on the very first write so writeRetry returns an error —
2163+ // exercises initStream's first `if err != nil { return err }` branch.
2164+ hub := & Hub {cfg : Config {RetryMS : 3000 }}
2165+ conn := newConnection ("c1" , []string {"t" }, 8 , 100 * time .Millisecond )
2166+ fw := & failingWriter {limit : 0 , err : errors .New ("forced write error" )}
2167+ err := hub .initStream (bufio .NewWriter (fw ), conn , "" )
2168+ require .Error (t , err )
2169+ }
2170+
2171+ func Test_SSE_ReplayEvents_HandlesReplayerError (t * testing.T ) {
2172+ t .Parallel ()
2173+
2174+ // Replayer returning an error must be treated as best-effort — caller
2175+ // gets nil and no events are written.
2176+ hub := & Hub {cfg : Config {Replayer : & errorReplayer {err : errors .New ("store down" )}}}
2177+ conn := newConnection ("c1" , []string {"t" }, 8 , 100 * time .Millisecond )
2178+ var buf bytes.Buffer
2179+ require .NoError (t , hub .replayEvents (bufio .NewWriter (& buf ), conn , "last-id" ))
2180+ require .Empty (t , buf .String ())
2181+ }
2182+
2183+ type errorReplayer struct { err error }
2184+
2185+ func (* errorReplayer ) Store (MarshaledEvent , []string ) error { return nil }
2186+ func (e * errorReplayer ) Replay (string , []string ) ([]MarshaledEvent , error ) {
2187+ return nil , e .err
2188+ }
2189+
2190+ func Test_SSE_ReplayEvents_WritesEventsAndFlushes (t * testing.T ) {
2191+ t .Parallel ()
2192+
2193+ // Replayer returning events must produce written frames terminated
2194+ // with a flush — exercises the write-and-flush branch.
2195+ r := & testReplayer {}
2196+ // testReplayer.Replay returns entries AFTER the lastEventID marker
2197+ // entry — store the marker first, then the two we want replayed.
2198+ require .NoError (t , r .Store (MarshaledEvent {ID : "last" , Type : "test" , Data : "anchor" }, []string {"t" }))
2199+ require .NoError (t , r .Store (MarshaledEvent {ID : "e1" , Type : "test" , Data : "one" }, []string {"t" }))
2200+ require .NoError (t , r .Store (MarshaledEvent {ID : "e2" , Type : "test" , Data : "two" }, []string {"t" }))
2201+
2202+ hub := & Hub {cfg : Config {Replayer : r }}
2203+ conn := newConnection ("c1" , []string {"t" }, 8 , 100 * time .Millisecond )
2204+ var buf bytes.Buffer
2205+ bw := bufio .NewWriter (& buf )
2206+ require .NoError (t , hub .replayEvents (bw , conn , "last" ))
2207+ require .NoError (t , bw .Flush ())
2208+ out := buf .String ()
2209+ require .Contains (t , out , "id: e1\n " )
2210+ require .Contains (t , out , "id: e2\n " )
2211+ require .Contains (t , out , "data: one\n " )
2212+ require .Contains (t , out , "data: two\n " )
2213+ }
2214+
2215+ func Test_SSE_SendConnectedEvent_PropagatesWriteError (t * testing.T ) {
2216+ t .Parallel ()
2217+
2218+ conn := newConnection ("c1" , []string {"t" }, 8 , 100 * time .Millisecond )
2219+ fw := & failingWriter {limit : 0 , err : errors .New ("no space" )}
2220+ err := sendConnectedEvent (bufio .NewWriter (fw ), conn )
2221+ require .Error (t , err )
2222+ }
2223+
2224+ func Test_SSE_WriteLoop_HeartbeatFlush (t * testing.T ) {
2225+ t .Parallel ()
2226+
2227+ // Drive writeLoop: fire a heartbeat then a real event, then close,
2228+ // so we hit the heartbeat branch, the normal event branch, and the
2229+ // done-exit branch — previously uncovered paths in writeLoop.
2230+ conn := newConnection ("c1" , []string {"t" }, 8 , 50 * time .Millisecond )
2231+ var buf bytes.Buffer
2232+ bw := bufio .NewWriter (& buf )
2233+
2234+ done := make (chan struct {})
2235+ go func () {
2236+ conn .writeLoop (bw )
2237+ close (done )
2238+ }()
2239+
2240+ conn .sendHeartbeat ()
2241+ // Heartbeat fires, wait briefly for flush.
2242+ time .Sleep (30 * time .Millisecond )
2243+
2244+ conn .trySend (MarshaledEvent {ID : "evt_x" , Type : "test" , Data : "hi" })
2245+ time .Sleep (30 * time .Millisecond )
2246+
2247+ conn .Close ()
2248+ <- done
2249+
2250+ require .NoError (t , bw .Flush ())
2251+ out := buf .String ()
2252+ require .Contains (t , out , ": heartbeat\n " , "heartbeat comment present" )
2253+ require .Contains (t , out , "data: hi\n " , "real event data present" )
2254+ require .Equal (t , int64 (1 ), conn .MessagesSent .Load (), "real event counted once" )
2255+ require .Equal (t , "evt_x" , conn .LastEventID .Load ())
2256+ }
0 commit comments