88 "net/http/httptest"
99 "os"
1010 "strings"
11+ "sync/atomic"
1112 "testing"
1213 "time"
1314
@@ -102,7 +103,7 @@ func TestFetchGitHubFeed_SendsBasicAuthHeader(t *testing.T) {
102103 defer srv .Close ()
103104 withMockGitHub (t , srv )
104105
105- if _ , err := fetchGitHubFeed (context .Background (), "octocat" ); err != nil {
106+ if _ , err := fetchGitHubFeed (context .Background (), "octocat" , time. Time {} ); err != nil {
106107 t .Fatalf ("fetchGitHubFeed: %v" , err )
107108 }
108109}
@@ -122,7 +123,7 @@ func TestFetchGitHubFeed_NoCredentials(t *testing.T) {
122123 defer srv .Close ()
123124 withMockGitHub (t , srv )
124125
125- if _ , err := fetchGitHubFeed (context .Background (), "octocat" ); err != nil {
126+ if _ , err := fetchGitHubFeed (context .Background (), "octocat" , time. Time {} ); err != nil {
126127 t .Fatalf ("fetchGitHubFeed: %v" , err )
127128 }
128129}
@@ -356,3 +357,175 @@ func TestSanpaiHandler_MissingAuthorizationHeader(t *testing.T) {
356357 t .Errorf ("status = %d, want 401" , rec .Code )
357358 }
358359}
360+
361+ // events を新しい順に n 件返すモックページを組み立てる(created_at は base から1分ずつ遡る)。
362+ func mockEventsPage (base time.Time , offset , n int ) string {
363+ items := make ([]string , 0 , n )
364+ for i := 0 ; i < n ; i ++ {
365+ at := base .Add (- time .Duration (offset + i ) * time .Minute ).UTC ().Format (time .RFC3339 )
366+ items = append (items , fmt .Sprintf (
367+ `{"id":"e%d","type":"PushEvent","created_at":%q,"repo":{"name":"o/r"},"payload":{}}` ,
368+ offset + i , at ))
369+ }
370+ return "[" + strings .Join (items , "," ) + "]"
371+ }
372+
373+ // 普段の参拝(前回から100件も動いていない)では1ページで打ち切ること。
374+ // ここが増えると全ユーザーのGitHub API呼び出しが毎回3倍になる。
375+ func TestFetchGitHubFeed_StopsAtFirstPageWhenCaughtUp (t * testing.T ) {
376+ now := time .Now ()
377+ since := now .Add (- 30 * time .Minute ) // 30件目より新しい
378+ var pages int32
379+
380+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
381+ atomic .AddInt32 (& pages , 1 )
382+ w .Header ().Set ("Content-Type" , "application/json" )
383+ // 1ページ丸ごと(100件)返す。ただし since 以前の分が含まれる。
384+ _ , _ = w .Write ([]byte (mockEventsPage (now , 0 , githubFeedPerPage )))
385+ }))
386+ defer srv .Close ()
387+ withMockGitHub (t , srv )
388+
389+ items , err := fetchGitHubFeed (context .Background (), "octocat" , since )
390+ if err != nil {
391+ t .Fatalf ("fetchGitHubFeed: %v" , err )
392+ }
393+ if got := atomic .LoadInt32 (& pages ); got != 1 {
394+ t .Errorf ("取得済みの範囲に達したら打ち切るべき: %d ページ取得した" , got )
395+ }
396+ if len (items ) != githubFeedPerPage {
397+ t .Errorf ("件数 = %d, want %d" , len (items ), githubFeedPerPage )
398+ }
399+ }
400+
401+ // 100件を超えて動いている場合は上限(3ページ=300件)まで遡ること。
402+ // これが無いと超過分を永久に取りこぼす(#239)。
403+ func TestFetchGitHubFeed_PaginatesUpToMaxPages (t * testing.T ) {
404+ now := time .Now ()
405+ since := now .Add (- 365 * 24 * time .Hour ) // どのページにも到達しない
406+ var pages int32
407+
408+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
409+ p := atomic .AddInt32 (& pages , 1 )
410+ if got := r .URL .Query ().Get ("page" ); got != fmt .Sprint (p ) {
411+ t .Errorf ("page パラメータ = %q, want %d" , got , p )
412+ }
413+ w .Header ().Set ("Content-Type" , "application/json" )
414+ _ , _ = w .Write ([]byte (mockEventsPage (now , int (p - 1 )* githubFeedPerPage , githubFeedPerPage )))
415+ }))
416+ defer srv .Close ()
417+ withMockGitHub (t , srv )
418+
419+ items , err := fetchGitHubFeed (context .Background (), "octocat" , since )
420+ if err != nil {
421+ t .Fatalf ("fetchGitHubFeed: %v" , err )
422+ }
423+ if got := atomic .LoadInt32 (& pages ); got != githubFeedMaxPages {
424+ t .Errorf ("ページ数 = %d, want %d (Events APIの上限)" , got , githubFeedMaxPages )
425+ }
426+ want := githubFeedPerPage * githubFeedMaxPages
427+ if len (items ) != want {
428+ t .Errorf ("件数 = %d, want %d" , len (items ), want )
429+ }
430+ }
431+
432+ // 埋まっていないページが来たらそこで終わり(存在しないページを叩かない)。
433+ func TestFetchGitHubFeed_StopsOnShortPage (t * testing.T ) {
434+ now := time .Now ()
435+ var pages int32
436+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
437+ p := atomic .AddInt32 (& pages , 1 )
438+ w .Header ().Set ("Content-Type" , "application/json" )
439+ if p == 1 {
440+ _ , _ = w .Write ([]byte (mockEventsPage (now , 0 , githubFeedPerPage )))
441+ return
442+ }
443+ _ , _ = w .Write ([]byte (mockEventsPage (now , githubFeedPerPage , 3 )))
444+ }))
445+ defer srv .Close ()
446+ withMockGitHub (t , srv )
447+
448+ items , err := fetchGitHubFeed (context .Background (), "octocat" , time.Time {})
449+ if err != nil {
450+ t .Fatalf ("fetchGitHubFeed: %v" , err )
451+ }
452+ if got := atomic .LoadInt32 (& pages ); got != 2 {
453+ t .Errorf ("ページ数 = %d, want 2 (2ページ目が埋まっていないので打ち切る)" , got )
454+ }
455+ if len (items ) != githubFeedPerPage + 3 {
456+ t .Errorf ("件数 = %d, want %d" , len (items ), githubFeedPerPage + 3 )
457+ }
458+ }
459+
460+ func TestReachedSince (t * testing.T ) {
461+ now := time .Now ()
462+ items := []feedItem {
463+ {Event : githubEvent {CreatedAt : now .Add (- 1 * time .Minute ).UTC ().Format (time .RFC3339 )}},
464+ {Event : githubEvent {CreatedAt : now .Add (- 10 * time .Minute ).UTC ().Format (time .RFC3339 )}},
465+ {Event : githubEvent {CreatedAt : "壊れた値" }},
466+ }
467+ if ! reachedSince (items , now .Add (- 5 * time .Minute )) {
468+ t .Errorf ("since 以前の要素があるので true のはず" )
469+ }
470+ if reachedSince (items , now .Add (- 60 * time .Minute )) {
471+ t .Errorf ("すべて since より新しいので false のはず" )
472+ }
473+ if reachedSince (nil , now ) {
474+ t .Errorf ("空なら false" )
475+ }
476+ }
477+
478+ // ページの取得中に新しいイベントが増えると窓がずれ、同じイベントが2つのページに
479+ // 載ることがある。重複したまま返すと、同一バッチ内で同じドキュメントへ2回書く
480+ // ことになり Firestore に弾かれる(参拝そのものが失敗する)うえ、ポイントと
481+ // 能力値も二重計上になる。
482+ func TestFetchGitHubFeed_DedupesAcrossPages (t * testing.T ) {
483+ now := time .Now ()
484+ var pages int32
485+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
486+ p := atomic .AddInt32 (& pages , 1 )
487+ w .Header ().Set ("Content-Type" , "application/json" )
488+ // 2ページとも同じ offset から返す = 全件重複する状況を作る。
489+ _ , _ = w .Write ([]byte (mockEventsPage (now , 0 , githubFeedPerPage )))
490+ _ = p
491+ }))
492+ defer srv .Close ()
493+ withMockGitHub (t , srv )
494+
495+ items , err := fetchGitHubFeed (context .Background (), "octocat" , time.Time {})
496+ if err != nil {
497+ t .Fatalf ("fetchGitHubFeed: %v" , err )
498+ }
499+ if len (items ) != githubFeedPerPage {
500+ t .Errorf ("重複を除いた件数 = %d, want %d" , len (items ), githubFeedPerPage )
501+ }
502+ seen := map [string ]bool {}
503+ for _ , it := range items {
504+ if seen [it .Event .ID ] {
505+ t .Fatalf ("重複したイベントIDが残っている: %s" , it .Event .ID )
506+ }
507+ seen [it .Event .ID ] = true
508+ }
509+ }
510+
511+ // 途中のページで失敗したら参拝ごと失敗させる(部分的に進めると last_sanpai が
512+ // 進んで、取れなかったイベントを二度と拾えなくなる)。
513+ func TestFetchGitHubFeed_FailsWhenLaterPageFails (t * testing.T ) {
514+ now := time .Now ()
515+ var pages int32
516+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
517+ p := atomic .AddInt32 (& pages , 1 )
518+ if p == 1 {
519+ w .Header ().Set ("Content-Type" , "application/json" )
520+ _ , _ = w .Write ([]byte (mockEventsPage (now , 0 , githubFeedPerPage )))
521+ return
522+ }
523+ w .WriteHeader (http .StatusInternalServerError )
524+ }))
525+ defer srv .Close ()
526+ withMockGitHub (t , srv )
527+
528+ if _ , err := fetchGitHubFeed (context .Background (), "octocat" , time.Time {}); err == nil {
529+ t .Errorf ("2ページ目が失敗したらエラーを返すべき(部分的に進めない)" )
530+ }
531+ }
0 commit comments