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:&quot;launched&quot;;s:19:&quot;2016-07-01 16:30:00&quot;;s:10:&quot;teacher_id&quot;;s:4:&quot;5982&quot;;s:9:&quot;lesson_id&quot;;s:8:&quot;25880364&quot;;}">予約可</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  }