@@ -9,6 +9,14 @@ import (
99 "time"
1010)
1111
12+ // Stub bodies must carry telegramSDKMarker — fetchTelegramSDK rejects anything that
13+ // doesn't look like the real wrapper, so a marker-less fixture would be discarded and
14+ // every test would read as a fetch failure.
15+ const (
16+ fakeSDK = "// WebView\n window.Telegram.WebApp = {platform:'test'};"
17+ fakeSDKFresh = "// WebView fresh\n window.Telegram.WebApp = {platform:'test2'};"
18+ )
19+
1220// stubTelegramSDKFetch swaps the upstream GET for the duration of a test and
1321// reports how many times it was called.
1422func stubTelegramSDKFetch (t * testing.T , fn func (ctx context.Context ) ([]byte , error )) * atomic.Int32 {
@@ -44,25 +52,41 @@ func waitTelegramSDKIdle(t *testing.T, m *Manager) {
4452 }
4553}
4654
55+ // waitTelegramSDKCalls waits until the stub has been entered `want` times and the
56+ // resulting fetch has finished. Needed because a background refresh is spawned with
57+ // `go`: waiting only for "no fetch in flight" can't tell "not scheduled yet" from
58+ // "already done", so the test would race ahead and observe zero calls.
59+ func waitTelegramSDKCalls (t * testing.T , m * Manager , calls * atomic.Int32 , want int32 ) {
60+ t .Helper ()
61+ deadline := time .Now ().Add (5 * time .Second )
62+ for calls .Load () < want {
63+ if time .Now ().After (deadline ) {
64+ t .Fatalf ("upstream called %d times after 5s, want %d" , calls .Load (), want )
65+ }
66+ time .Sleep (5 * time .Millisecond )
67+ }
68+ waitTelegramSDKIdle (t , m )
69+ }
70+
4771// A cold cache must fetch INLINE and hand the real body to the very first caller —
4872// that's the whole point of the cold path (an empty file would silently disable the
4973// Mini App bridge for whoever loads the page first).
5074func TestTelegramSDKColdFetchesInline (t * testing.T ) {
5175 calls := stubTelegramSDKFetch (t , func (context.Context ) ([]byte , error ) {
52- return []byte ("// WebView" ), nil
76+ return []byte (fakeSDK ), nil
5377 })
5478 m := & Manager {}
5579
5680 body , ok := m .TelegramWebAppSDK ()
57- if ! ok || string (body ) != "// WebView" {
81+ if ! ok || string (body ) != fakeSDK {
5882 t .Fatalf ("cold read: got (%q, %v), want the fetched body" , body , ok )
5983 }
6084 if got := calls .Load (); got != 1 {
6185 t .Fatalf ("upstream called %d times, want 1" , got )
6286 }
6387
6488 // Second read is served from cache — no new fetch.
65- if body , ok = m .TelegramWebAppSDK (); ! ok || string (body ) != "// WebView" {
89+ if body , ok = m .TelegramWebAppSDK (); ! ok || string (body ) != fakeSDK {
6690 t .Fatalf ("warm read: got (%q, %v)" , body , ok )
6791 }
6892 if got := calls .Load (); got != 1 {
@@ -115,7 +139,7 @@ func TestTelegramSDKConcurrentColdSingleflight(t *testing.T) {
115139 release := make (chan struct {})
116140 calls := stubTelegramSDKFetch (t , func (context.Context ) ([]byte , error ) {
117141 <- release // hold the fetch open so every goroutine piles up behind it
118- return []byte ("// WebView" ), nil
142+ return []byte (fakeSDK ), nil
119143 })
120144 m := & Manager {}
121145
@@ -127,7 +151,7 @@ func TestTelegramSDKConcurrentColdSingleflight(t *testing.T) {
127151 go func () {
128152 defer wg .Done ()
129153 body , ok := m .TelegramWebAppSDK ()
130- got [i ] = ok && string (body ) == "// WebView"
154+ got [i ] = ok && string (body ) == fakeSDK
131155 }()
132156 }
133157 time .Sleep (50 * time .Millisecond ) // let them all reach the cold path
@@ -144,6 +168,59 @@ func TestTelegramSDKConcurrentColdSingleflight(t *testing.T) {
144168 }
145169}
146170
171+ // A stale copy plus a failing upstream must NOT become a retry loop. A failed fetch
172+ // never advances tgSDKAt, so the copy stays stale and every request re-triggers the
173+ // refresh — without the cooldown guard the panel dials a blocked telegram.org
174+ // back-to-back for as long as the page sees traffic.
175+ func TestTelegramSDKStaleFailureDoesNotLoop (t * testing.T ) {
176+ calls := stubTelegramSDKFetch (t , func (context.Context ) ([]byte , error ) {
177+ return nil , errors .New ("dial tcp: i/o timeout" )
178+ })
179+ m := & Manager {}
180+ m .tgSDKBody = []byte ("old" )
181+ m .tgSDKAt = time .Now ().Add (- telegramSDKTTL - time .Minute ) // stale
182+
183+ // First read kicks off a refresh, which fails and arms the cooldown.
184+ if body , ok := m .TelegramWebAppSDK (); ! ok || string (body ) != "old" {
185+ t .Fatalf ("stale read: got (%q, %v), want the old body" , body , ok )
186+ }
187+ waitTelegramSDKCalls (t , m , calls , 1 )
188+
189+ // Hammer it: still stale, still failing — the cooldown must absorb every one.
190+ for range 50 {
191+ if body , ok := m .TelegramWebAppSDK (); ! ok || string (body ) != "old" {
192+ t .Fatalf ("stale read regressed: got (%q, %v)" , body , ok )
193+ }
194+ }
195+ waitTelegramSDKIdle (t , m )
196+ if got := calls .Load (); got != 1 {
197+ t .Fatalf ("stale+failing upstream looped: %d upstream calls after 51 reads, want 1" , got )
198+ }
199+ }
200+
201+ // A 200 that isn't actually the SDK (a transparent-proxy block page, or a body
202+ // truncated at the size cap — which returns no error) must not be cached: it would
203+ // be served as JS to every user for a full TTL.
204+ func TestTelegramSDKRejectsNonSDKBody (t * testing.T ) {
205+ stubTelegramSDKFetch (t , func (context.Context ) ([]byte , error ) {
206+ return []byte ("<html><body>Access denied</body></html>" ), nil
207+ })
208+ m := & Manager {}
209+
210+ if body , ok := m .TelegramWebAppSDK (); ok || body != nil {
211+ t .Fatalf ("garbage body was accepted: got (%q, %v), want (nil, false)" , body , ok )
212+ }
213+ m .tgSDKMu .Lock ()
214+ cached , failed := m .tgSDKBody , ! m .tgSDKFailAt .IsZero ()
215+ m .tgSDKMu .Unlock ()
216+ if cached != nil {
217+ t .Errorf ("garbage body got cached: %q" , cached )
218+ }
219+ if ! failed {
220+ t .Error ("a rejected body must arm the failure cooldown like any other failure" )
221+ }
222+ }
223+
147224// A stale copy is served immediately (never blocking the page) while the refresh
148225// happens behind it.
149226func TestTelegramSDKStaleServedImmediately (t * testing.T ) {
@@ -152,7 +229,7 @@ func TestTelegramSDKStaleServedImmediately(t *testing.T) {
152229 stubTelegramSDKFetch (t , func (context.Context ) ([]byte , error ) {
153230 once .Do (func () { close (started ) })
154231 <- block
155- return []byte ("fresh" ), nil
232+ return []byte (fakeSDKFresh ), nil
156233 })
157234 m := & Manager {}
158235 m .tgSDKBody = []byte ("old" )
@@ -180,7 +257,7 @@ func TestTelegramSDKStaleServedImmediately(t *testing.T) {
180257 waitTelegramSDKIdle (t , m ) // let it land before cleanup restores the stub
181258
182259 // ...and the refresh it kicked off replaced the stale copy.
183- if body , ok := m .TelegramWebAppSDK (); ! ok || string (body ) != "fresh" {
260+ if body , ok := m .TelegramWebAppSDK (); ! ok || string (body ) != fakeSDKFresh {
184261 t .Fatalf ("after refresh: got (%q, %v), want the fresh body" , body , ok )
185262 }
186263}
0 commit comments