github.com/oinume/lekcije@v0.0.0-20231017100347-5b4c5eb6ab24/backend/infrastructure/dmm_eikaiwa/lesson_fetcher_test.go (about) 1 package dmm_eikaiwa 2 3 import ( 4 "context" 5 "flag" 6 "fmt" 7 "io" 8 "math/big" 9 "net/http" 10 "os" 11 "strings" 12 "sync" 13 "testing" 14 "time" 15 16 "github.com/ericlagergren/decimal" 17 "github.com/google/go-cmp/cmp" 18 "github.com/google/go-cmp/cmp/cmpopts" 19 "github.com/volatiletech/sqlboiler/v4/types" 20 "go.uber.org/zap" 21 22 "github.com/oinume/lekcije/backend/domain/config" 23 "github.com/oinume/lekcije/backend/errors" 24 "github.com/oinume/lekcije/backend/infrastructure/mysql" 25 "github.com/oinume/lekcije/backend/internal/assertion" 26 "github.com/oinume/lekcije/backend/internal/mock" 27 "github.com/oinume/lekcije/backend/internal/modeltest" 28 "github.com/oinume/lekcije/backend/model" 29 "github.com/oinume/lekcije/backend/model2" 30 ) 31 32 var ( 33 concurrency = flag.Int("concurrency", 1, "concurrency") 34 mCountryList *model2.MCountryList 35 ) 36 37 func TestMain(m *testing.M) { 38 config.MustProcessDefault() 39 testDBURL := model.ReplaceToTestDBURL(nil, config.DefaultVars.DBURL()) 40 var err error 41 db, err := model.OpenDB(testDBURL, 1, config.DefaultVars.DebugSQL) 42 if err != nil { 43 panic(err) 44 } 45 46 ctx := context.Background() 47 mCountries, err := mysql.NewMCountryRepository(db.DB()).FindAll(ctx) 48 if err != nil { 49 panic(err) 50 } 51 mCountryList = model2.NewMCountryList(mCountries) 52 53 os.Exit(m.Run()) 54 } 55 56 func Test_lessonFetcher_Fetch(t *testing.T) { 57 transport := &errorTransport{okThreshold: 0} 58 httpClient := &http.Client{Transport: transport} 59 fetcher := NewLessonFetcher(httpClient, *concurrency, false, mCountryList, zap.NewNop()) 60 ctx := context.Background() 61 teacher, lessons, err := fetcher.Fetch(ctx, 49393) 62 if err != nil { 63 t.Fatalf("fetcher.Fetch failed: %v", err) 64 } 65 66 assertion.AssertEqual(t, "Judith(ジュディス)", teacher.Name, "") 67 assertion.AssertEqual(t, 38, len(lessons), "") 68 assertion.AssertEqual(t, 1, transport.callCount, "") 69 } 70 71 func Test_lessonFetcher_Fetch_Retry(t *testing.T) { 72 transport := &errorTransport{okThreshold: 2} 73 client := &http.Client{Transport: transport} 74 fetcher := NewLessonFetcher(client, 1, false, mCountryList, zap.NewNop()) 75 teacher, _, err := fetcher.Fetch(context.Background(), 49393) 76 if err != nil { 77 t.Fatalf("fetcher.Fetch failed: %v", err) 78 } 79 80 assertion.AssertEqual(t, "Judith(ジュディス)", teacher.Name, "") 81 assertion.AssertEqual(t, 2, transport.callCount, "") 82 } 83 84 func Test_lessonFetcher_Fetch_Redirect(t *testing.T) { 85 client := &http.Client{ 86 Transport: &redirectTransport{}, 87 CheckRedirect: redirectErrorFunc, 88 } 89 fetcher := NewLessonFetcher(client, 1, false, mCountryList, zap.NewNop()) 90 _, _, err := fetcher.Fetch(context.Background(), 5982) 91 if err == nil { 92 t.Fatalf("err must not be nil") 93 } 94 95 assertion.AssertEqual(t, true, errors.IsNotFound(err), "") 96 } 97 98 func Test_lessonFetcher_Fetch_InternalServerError(t *testing.T) { 99 client := &http.Client{ 100 Transport: &responseTransport{ 101 statusCode: http.StatusInternalServerError, 102 content: "Internal Server Error", 103 }, 104 } 105 fetcher := NewLessonFetcher(client, 1, false, mCountryList, zap.NewNop()) 106 _, _, err := fetcher.Fetch(context.Background(), 5982) 107 if err == nil { 108 t.Fatalf("err must not be nil") 109 } 110 111 wantTexts := []string{ 112 "Unknown error in fetchContent", 113 "statusCode=500", 114 } 115 for _, want := range wantTexts { 116 if !strings.Contains(err.Error(), want) { 117 t.Fatalf("err %q doesn't contain text %q", err, want) 118 } 119 } 120 } 121 122 func Test_lessonFetcher_Fetch_Concurrency(t *testing.T) { 123 mockTransport, err := mock.NewHTMLTransport("testdata/49393.html") 124 if err != nil { 125 t.Fatal(err) 126 } 127 client := &http.Client{Transport: mockTransport} 128 fetcher := NewLessonFetcher(client, *concurrency, false, mCountryList, zap.NewNop()) 129 130 const n = 500 131 wg := &sync.WaitGroup{} 132 for i := 0; i < n; i++ { 133 wg.Add(1) 134 go func(teacherID int) { 135 defer wg.Done() 136 _, _, err := fetcher.Fetch(context.Background(), uint(teacherID)) 137 if err != nil { 138 fmt.Printf("err = %v\n", err) 139 return 140 } 141 }(i) 142 } 143 wg.Wait() 144 145 assertion.AssertEqual(t, n, mockTransport.NumCalled, "") 146 } 147 148 func Test_lessonFetcher_parseHTML(t *testing.T) { 149 fetcher := NewLessonFetcher(http.DefaultClient, 1, false, mCountryList, zap.NewNop()).(*lessonFetcher) 150 file, err := os.Open("testdata/49393.html") 151 if err != nil { 152 t.Fatal(err) 153 } 154 t.Cleanup(func() { 155 _ = file.Close() 156 }) 157 158 gotTeacher, lessons, err := fetcher.parseHTML(model2.NewTeacher(uint(49393)), file) 159 if err != nil { 160 t.Fatalf("fetcher.parseHTML failed: %v", err) 161 } 162 wantTeacher := modeltest.NewTeacher(func(teacher *model2.Teacher) { 163 teacher.ID = 49393 164 teacher.Name = "Judith(ジュディス)" 165 teacher.CountryID = int16(608) 166 teacher.Birthday = time.Time{} 167 teacher.YearsOfExperience = 2 168 teacher.FavoriteCount = 559 169 teacher.ReviewCount = 1267 170 teacher.Rating = types.NullDecimal{Big: decimal.New(int64(498), 2)} 171 //teacher.LastLessonAt = time.Date(2022, 12, 31, 10, 30, 0, 0, time.UTC) 172 }) 173 assertion.AssertEqual( 174 t, wantTeacher, gotTeacher, "", 175 cmp.AllowUnexported(decimal.Big{}, big.Int{}), 176 cmpopts.IgnoreFields(model2.Teacher{}, "LastLessonAt"), 177 ) 178 179 assertion.AssertEqual(t, true, len(lessons) > 0, "num of lessons must be greater than zero") 180 const dtFormat = "2006-01-02 15:04" 181 for _, lesson := range lessons { 182 if lesson.Datetime.Format(dtFormat) == "2018-03-01 18:00" { 183 assertion.AssertEqual(t, "Finished", lesson.Status, "") 184 } 185 if lesson.Datetime.Format(dtFormat) == "2018-03-03 06:30" { 186 assertion.AssertEqual(t, "Available", lesson.Status, "") 187 } 188 if lesson.Datetime.Format(dtFormat) == "2018-03-03 02:00" { 189 assertion.AssertEqual(t, "Reserved", lesson.Status, "") 190 } 191 } 192 } 193 194 //<a href="#" class="bt-open" id="a:3:{s:8:"launched";s:19:"2016-07-01 16:30:00";s:10:"teacher_id";s:4:"5982";s:9:"lesson_id";s:8:"25880364";}">予約可</a> 195 196 type errorTransport struct { 197 okThreshold int 198 callCount int 199 } 200 201 func (t *errorTransport) RoundTrip(req *http.Request) (*http.Response, error) { 202 t.callCount++ 203 if t.callCount < t.okThreshold { 204 return nil, fmt.Errorf("Please retry.") 205 } 206 207 resp := &http.Response{ 208 Header: make(http.Header), 209 Request: req, 210 StatusCode: http.StatusOK, 211 Status: "200 OK", 212 } 213 resp.Header.Set("Content-Type", "text/html; charset=UTF-8") 214 215 file, err := os.Open("testdata/49393.html") 216 if err != nil { 217 return nil, err 218 } 219 resp.Body = file // Close() will be called by client 220 return resp, nil 221 } 222 223 type redirectTransport struct{} 224 225 func (t *redirectTransport) RoundTrip(req *http.Request) (*http.Response, error) { 226 resp := &http.Response{ 227 Header: make(http.Header), 228 Request: req, 229 StatusCode: http.StatusFound, 230 Status: "302 Found", 231 Body: io.NopCloser(strings.NewReader("")), 232 } 233 resp.Header.Set("Location", "https://twitter.com/") 234 return resp, nil 235 } 236 237 type responseTransport struct { 238 statusCode int 239 status string 240 content string 241 } 242 243 func (t *responseTransport) RoundTrip(req *http.Request) (*http.Response, error) { 244 resp := &http.Response{ 245 Header: make(http.Header), 246 Request: req, 247 StatusCode: t.statusCode, 248 Status: t.status, 249 Body: io.NopCloser(strings.NewReader(t.content)), 250 } 251 return resp, nil 252 }