From 59ea39b9f3831ed22d782cf51dcd610e13897442 Mon Sep 17 00:00:00 2001 From: Tsvetoslav Dimov Date: Sun, 26 Nov 2023 16:42:09 +0000 Subject: [PATCH] tests: wrote imdb client unit tests --- go.mod | 4 + go.sum | 10 + pkg/client/imdb.go | 16 +- pkg/client/imdb_test.go | 676 +++++++++++++++++++++++++++ pkg/client/testdata/imdb_list.csv | 4 + pkg/client/testdata/imdb_lists.html | 8 + pkg/client/testdata/imdb_ratings.csv | 4 + pkg/logger/slog.go | 6 +- pkg/syncer/syncer.go | 2 +- 9 files changed, 719 insertions(+), 11 deletions(-) create mode 100644 pkg/client/imdb_test.go create mode 100644 pkg/client/testdata/imdb_list.csv create mode 100644 pkg/client/testdata/imdb_lists.html create mode 100644 pkg/client/testdata/imdb_ratings.csv diff --git a/go.mod b/go.mod index 3cda54c..24e01a6 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,13 @@ go 1.21 require ( github.com/PuerkitoBio/goquery v1.8.1 github.com/joho/godotenv v1.5.1 + github.com/stretchr/testify v1.8.4 ) require ( github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/net v0.17.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fd2d44b..249563d 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,14 @@ github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAc github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -36,3 +42,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/client/imdb.go b/pkg/client/imdb.go index ac15992..f1f7899 100644 --- a/pkg/client/imdb.go +++ b/pkg/client/imdb.go @@ -41,6 +41,7 @@ type ImdbClient struct { } type ImdbConfig struct { + BasePath string CookieAtMain string CookieUbidMain string UserId string @@ -48,6 +49,7 @@ type ImdbConfig struct { } func NewImdbClient(config ImdbConfig, logger *slog.Logger) (ImdbClientInterface, error) { + config.BasePath = imdbPathBase jar, err := setupCookieJar(config) if err != nil { return nil, err @@ -66,9 +68,9 @@ func NewImdbClient(config ImdbConfig, logger *slog.Logger) (ImdbClientInterface, } func setupCookieJar(config ImdbConfig) (http.CookieJar, error) { - imdbUrl, err := url.Parse(imdbPathBase) + imdbUrl, err := url.Parse(config.BasePath) if err != nil { - return nil, fmt.Errorf("failure parsing %s as url: %w", imdbPathBase, err) + return nil, fmt.Errorf("failure parsing %s as url: %w", config.BasePath, err) } jar, err := cookiejar.New(nil) if err != nil { @@ -133,7 +135,7 @@ func (c *ImdbClient) doRequest(requestFields requestFields) (*http.Response, err func (c *ImdbClient) ListGet(listId string) (*entities.ImdbList, error) { response, err := c.doRequest(requestFields{ Method: http.MethodGet, - BasePath: imdbPathBase, + BasePath: c.config.BasePath, Endpoint: fmt.Sprintf(imdbPathListExport, listId), Body: http.NoBody, }) @@ -163,7 +165,7 @@ func (c *ImdbClient) WatchlistGet() (*entities.ImdbList, error) { func (c *ImdbClient) ListsGetAll() ([]entities.ImdbList, error) { response, err := c.doRequest(requestFields{ Method: http.MethodGet, - BasePath: imdbPathBase, + BasePath: c.config.BasePath, Endpoint: fmt.Sprintf(imdbPathLists, c.config.UserId), Body: http.NoBody, }) @@ -232,7 +234,7 @@ func (c *ImdbClient) ListsGet(listIds []string) ([]entities.ImdbList, error) { func (c *ImdbClient) UserIdScrape() error { response, err := c.doRequest(requestFields{ Method: http.MethodGet, - BasePath: imdbPathBase, + BasePath: c.config.BasePath, Endpoint: imdbPathProfile, Body: http.NoBody, }) @@ -250,7 +252,7 @@ func (c *ImdbClient) UserIdScrape() error { func (c *ImdbClient) WatchlistIdScrape() error { response, err := c.doRequest(requestFields{ Method: http.MethodGet, - BasePath: imdbPathBase, + BasePath: c.config.BasePath, Endpoint: imdbPathWatchlist, Body: http.NoBody, }) @@ -268,7 +270,7 @@ func (c *ImdbClient) WatchlistIdScrape() error { func (c *ImdbClient) RatingsGet() ([]entities.ImdbItem, error) { response, err := c.doRequest(requestFields{ Method: http.MethodGet, - BasePath: imdbPathBase, + BasePath: c.config.BasePath, Endpoint: fmt.Sprintf(imdbPathRatingsExport, c.config.UserId), Body: http.NoBody, }) diff --git a/pkg/client/imdb_test.go b/pkg/client/imdb_test.go new file mode 100644 index 0000000..b04739b --- /dev/null +++ b/pkg/client/imdb_test.go @@ -0,0 +1,676 @@ +package client + +import ( + "io" + "net/http" + "net/http/httptest" + "os" + "sort" + "testing" + + "github.com/cecobask/imdb-trakt-sync/pkg/entities" + "github.com/cecobask/imdb-trakt-sync/pkg/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func populateHttpResponseWithFileContents(w http.ResponseWriter, filename string) error { + f, err := os.ReadFile(filename) + if err != nil { + return err + } + _, err = w.Write(f) + if err != nil { + return err + } + return nil +} + +func TestImdbClient_doRequest(t *testing.T) { + type args struct { + requestFields requestFields + } + dummyRequestFields := requestFields{ + Method: http.MethodGet, + Endpoint: "/", + } + tests := []struct { + name string + args args + requirements func(*require.Assertions) *httptest.Server + assertions func(*assert.Assertions, *http.Response, error) + }{ + { + name: "handle status ok", + args: args{ + requestFields: dummyRequestFields, + }, + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(dummyRequestFields.Method, r.Method) + requirements.Equal(dummyRequestFields.Endpoint, r.URL.Path) + w.WriteHeader(http.StatusOK) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, res *http.Response, err error) { + assertions.NotNil(res) + assertions.NoError(err) + assertions.Equal(http.StatusOK, res.StatusCode) + }, + }, + { + name: "handle status not found", + args: args{ + requestFields: dummyRequestFields, + }, + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(dummyRequestFields.Method, r.Method) + requirements.Equal(dummyRequestFields.Endpoint, r.URL.Path) + w.WriteHeader(http.StatusNotFound) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, res *http.Response, err error) { + assertions.NotNil(res) + assertions.NoError(err) + assertions.Equal(http.StatusNotFound, res.StatusCode) + }, + }, + { + name: "handle status forbidden", + args: args{ + requestFields: dummyRequestFields, + }, + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(dummyRequestFields.Method, r.Method) + requirements.Equal(dummyRequestFields.Endpoint, r.URL.Path) + w.WriteHeader(http.StatusForbidden) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, res *http.Response, err error) { + assertions.Nil(res) + assertions.Error(err) + }, + }, + { + name: "handle unexpected status", + args: args{ + requestFields: dummyRequestFields, + }, + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(dummyRequestFields.Method, r.Method) + requirements.Equal(dummyRequestFields.Endpoint, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, res *http.Response, err error) { + assertions.Nil(res) + assertions.Error(err) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testServer := tt.requirements(require.New(t)) + defer testServer.Close() + tt.args.requestFields.BasePath = testServer.URL + c := &ImdbClient{ + client: http.DefaultClient, + } + res, err := c.doRequest(tt.args.requestFields) + tt.assertions(assert.New(t), res, err) + }) + } +} + +func TestImdbClient_ListGet(t *testing.T) { + type args struct { + listId string + } + tests := []struct { + name string + args args + requirements func(*require.Assertions) *httptest.Server + assertions func(*assert.Assertions, *entities.ImdbList, error) + }{ + { + name: "successfully get list", + args: args{ + listId: "ls123456789", + }, + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal("/list/ls123456789/export", r.URL.Path) + w.Header().Set(imdbHeaderKeyContentDisposition, `attachment; filename="Watched (2023).csv"`) + w.WriteHeader(http.StatusOK) + requirements.NoError(populateHttpResponseWithFileContents(w, "testdata/imdb_list.csv")) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, list *entities.ImdbList, err error) { + assertions.NotNil(list) + assertions.NoError(err) + assertions.Equal("ls123456789", list.ListId) + assertions.Equal("Watched (2023)", list.ListName) + assertions.Equal(3, len(list.ListItems)) + assertions.Equal(false, list.IsWatchlist) + assertions.Equal("watched-2023", list.TraktListSlug) + }, + }, + { + name: "handle error when list is not found", + args: args{ + listId: "ls123456789", + }, + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal("/list/ls123456789/export", r.URL.Path) + w.WriteHeader(http.StatusNotFound) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, list *entities.ImdbList, err error) { + assertions.Nil(list) + assertions.Error(err) + }, + }, + { + name: "handle unexpected status", + args: args{ + listId: "ls123456789", + }, + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal("/list/ls123456789/export", r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, list *entities.ImdbList, err error) { + assertions.Nil(list) + assertions.Error(err) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testServer := tt.requirements(require.New(t)) + defer testServer.Close() + c := &ImdbClient{ + client: http.DefaultClient, + config: ImdbConfig{ + BasePath: testServer.URL, + }, + } + list, err := c.ListGet(tt.args.listId) + tt.assertions(assert.New(t), list, err) + }) + } +} + +func TestImdbClient_WatchlistGet(t *testing.T) { + tests := []struct { + name string + requirements func(*require.Assertions) *httptest.Server + assertions func(*assert.Assertions, *entities.ImdbList, error) + }{ + { + name: "successfully get watchlist", + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal("/list/ls123456789/export", r.URL.Path) + w.Header().Set(imdbHeaderKeyContentDisposition, `attachment; filename="WATCHLIST.csv"`) + w.WriteHeader(http.StatusOK) + requirements.NoError(populateHttpResponseWithFileContents(w, "testdata/imdb_list.csv")) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, list *entities.ImdbList, err error) { + assertions.NotNil(list) + assertions.NoError(err) + assertions.Equal("ls123456789", list.ListId) + assertions.Equal("WATCHLIST", list.ListName) + assertions.Equal(3, len(list.ListItems)) + assertions.Equal(true, list.IsWatchlist) + assertions.Equal("watchlist", list.TraktListSlug) + }, + }, + { + name: "fail to get watchlist", + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal("/list/ls123456789/export", r.URL.Path) + w.WriteHeader(http.StatusNotFound) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, list *entities.ImdbList, err error) { + assertions.Nil(list) + assertions.Error(err) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testServer := tt.requirements(require.New(t)) + defer testServer.Close() + c := &ImdbClient{ + client: http.DefaultClient, + config: ImdbConfig{ + BasePath: testServer.URL, + WatchlistId: "ls123456789", + }, + } + list, err := c.WatchlistGet() + tt.assertions(assert.New(t), list, err) + }) + } +} + +func TestImdbClient_ListsGetAll(t *testing.T) { + tests := []struct { + name string + requirements func(*require.Assertions) *httptest.Server + assertions func(*assert.Assertions, []entities.ImdbList, error) + }{ + { + name: "successfully get all lists", + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + allowedPaths := map[string]bool{ + "/user/ur12345678/lists": true, + "/list/ls123456789/export": true, + "/list/ls987654321/export": true, + } + requirements.Equal(http.MethodGet, r.Method) + requirements.True(allowedPaths[r.URL.Path]) + filename := "testdata/imdb_list.csv" + if r.URL.Path == "/user/ur12345678/lists" { + filename = "testdata/imdb_lists.html" + } + w.Header().Set(imdbHeaderKeyContentDisposition, `attachment; filename="DummyList.csv"`) + w.WriteHeader(http.StatusOK) + requirements.NoError(populateHttpResponseWithFileContents(w, filename)) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, lists []entities.ImdbList, err error) { + assertions.NotNil(lists) + assertions.NoError(err) + assertions.Equal(2, len(lists)) + sort.Slice(lists, func(a, b int) bool { + return lists[a].ListId < lists[b].ListId + }) + assertions.Equal("ls123456789", lists[0].ListId) + assertions.Equal("ls987654321", lists[1].ListId) + }, + }, + { + name: "fail to get all lists", + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal("/user/ur12345678/lists", r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, lists []entities.ImdbList, err error) { + assertions.Nil(lists) + assertions.Error(err) + }, + }, + { + name: "fail to find lists in html response", + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal("/user/ur12345678/lists", r.URL.Path) + w.WriteHeader(http.StatusOK) + bytes, err := w.Write([]byte(``)) + requirements.Greater(bytes, 0) + requirements.NoError(err) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, lists []entities.ImdbList, err error) { + assertions.NotNil(lists) + assertions.Equal(0, len(lists)) + assertions.NoError(err) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testServer := tt.requirements(require.New(t)) + defer testServer.Close() + c := &ImdbClient{ + client: http.DefaultClient, + config: ImdbConfig{ + BasePath: testServer.URL, + UserId: "ur12345678", + }, + logger: logger.NewLogger(io.Discard), + } + lists, err := c.ListsGetAll() + tt.assertions(assert.New(t), lists, err) + }) + } +} + +func TestImdbClient_ListsGet(t *testing.T) { + type args struct { + listIds []string + } + tests := []struct { + name string + args args + requirements func(*require.Assertions) *httptest.Server + assertions func(*assert.Assertions, []entities.ImdbList, error) + }{ + { + name: "successfully get lists", + args: args{ + listIds: []string{ + "ls123456789", + "ls987654321", + }, + }, + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + allowedPaths := map[string]bool{ + "/list/ls123456789/export": true, + "/list/ls987654321/export": true, + } + requirements.Equal(http.MethodGet, r.Method) + requirements.True(allowedPaths[r.URL.Path]) + w.Header().Set(imdbHeaderKeyContentDisposition, `attachment; filename="DummyList.csv"`) + w.WriteHeader(http.StatusOK) + requirements.NoError(populateHttpResponseWithFileContents(w, "testdata/imdb_list.csv")) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, lists []entities.ImdbList, err error) { + assertions.NotNil(lists) + assertions.NoError(err) + assertions.Equal(2, len(lists)) + sort.Slice(lists, func(a, b int) bool { + return lists[a].ListId < lists[b].ListId + }) + assertions.Equal("ls123456789", lists[0].ListId) + assertions.Equal("ls987654321", lists[1].ListId) + }, + }, + { + name: "silently ignore lists that could not be found", + args: args{ + listIds: []string{ + "ls123456789", + }, + }, + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal("/list/ls123456789/export", r.URL.Path) + w.WriteHeader(http.StatusNotFound) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, lists []entities.ImdbList, err error) { + assertions.NotNil(lists) + assertions.NoError(err) + assertions.Equal(0, len(lists)) + }, + }, + { + name: "handle unexpected error when getting lists", + args: args{ + listIds: []string{ + "ls123456789", + }, + }, + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal("/list/ls123456789/export", r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, lists []entities.ImdbList, err error) { + assertions.Nil(lists) + assertions.Error(err) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testServer := tt.requirements(require.New(t)) + defer testServer.Close() + c := &ImdbClient{ + client: http.DefaultClient, + config: ImdbConfig{ + BasePath: testServer.URL, + UserId: "ur12345678", + }, + logger: logger.NewLogger(io.Discard), + } + lists, err := c.ListsGet(tt.args.listIds) + tt.assertions(assert.New(t), lists, err) + }) + } +} + +func TestImdbClient_UserIdScrape(t *testing.T) { + tests := []struct { + name string + requirements func(*require.Assertions) *httptest.Server + assertions func(*assert.Assertions, *ImdbClient, error) + }{ + { + name: "successfully scrape user id", + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal(imdbPathProfile, r.URL.Path) + w.WriteHeader(http.StatusOK) + bytes, err := w.Write([]byte(`
`)) + requirements.Greater(bytes, 0) + requirements.NoError(err) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, c *ImdbClient, err error) { + assertions.NotNil(c) + assertions.NoError(err) + assertions.Equal("ur12345678", c.config.UserId) + }, + }, + { + name: "handle unexpected status", + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal(imdbPathProfile, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, c *ImdbClient, err error) { + assertions.Zero(c.config.UserId) + assertions.Error(err) + }, + }, + { + name: "fail to scrape user id", + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal(imdbPathProfile, r.URL.Path) + w.WriteHeader(http.StatusOK) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, c *ImdbClient, err error) { + assertions.Zero(c.config.UserId) + assertions.Error(err) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testServer := tt.requirements(require.New(t)) + defer testServer.Close() + c := &ImdbClient{ + client: http.DefaultClient, + config: ImdbConfig{ + BasePath: testServer.URL, + }, + } + err := c.UserIdScrape() + tt.assertions(assert.New(t), c, err) + }) + } +} + +func TestImdbClient_WatchlistIdScrape(t *testing.T) { + tests := []struct { + name string + requirements func(*require.Assertions) *httptest.Server + assertions func(*assert.Assertions, *ImdbClient, error) + }{ + { + name: "successfully scrape watchlist id", + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal(imdbPathWatchlist, r.URL.Path) + w.WriteHeader(http.StatusOK) + bytes, err := w.Write([]byte(``)) + requirements.Greater(bytes, 0) + requirements.NoError(err) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, c *ImdbClient, err error) { + assertions.NotNil(c) + assertions.NoError(err) + assertions.Equal("ls123456789", c.config.WatchlistId) + }, + }, + { + name: "handle unexpected status", + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal(imdbPathWatchlist, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, c *ImdbClient, err error) { + assertions.Zero(c.config.WatchlistId) + assertions.Error(err) + }, + }, + { + name: "fail to scrape watchlist id", + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal(imdbPathWatchlist, r.URL.Path) + w.WriteHeader(http.StatusOK) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, c *ImdbClient, err error) { + assertions.Zero(c.config.WatchlistId) + assertions.Error(err) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testServer := tt.requirements(require.New(t)) + defer testServer.Close() + c := &ImdbClient{ + client: http.DefaultClient, + config: ImdbConfig{ + BasePath: testServer.URL, + }, + } + err := c.WatchlistIdScrape() + tt.assertions(assert.New(t), c, err) + }) + } +} + +func TestImdbClient_RatingsGet(t *testing.T) { + tests := []struct { + name string + requirements func(*require.Assertions) *httptest.Server + assertions func(*assert.Assertions, []entities.ImdbItem, error) + }{ + { + name: "successfully get ratings", + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal("/user/ur12345678/ratings/export", r.URL.Path) + w.WriteHeader(http.StatusOK) + requirements.NoError(populateHttpResponseWithFileContents(w, "testdata/imdb_ratings.csv")) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, ratings []entities.ImdbItem, err error) { + assertions.NotNil(ratings) + assertions.NoError(err) + assertions.Equal(3, len(ratings)) + assertions.Equal("tt5013056", ratings[0].Id) + assertions.Equal("tt15398776", ratings[1].Id) + assertions.Equal("tt0172495", ratings[2].Id) + }, + }, + { + name: "handle unexpected status", + requirements: func(requirements *require.Assertions) *httptest.Server { + handler := func(w http.ResponseWriter, r *http.Request) { + requirements.Equal(http.MethodGet, r.Method) + requirements.Equal("/user/ur12345678/ratings/export", r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + return httptest.NewServer(http.HandlerFunc(handler)) + }, + assertions: func(assertions *assert.Assertions, ratings []entities.ImdbItem, err error) { + assertions.Nil(ratings) + assertions.Error(err) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testServer := tt.requirements(require.New(t)) + defer testServer.Close() + c := &ImdbClient{ + client: http.DefaultClient, + config: ImdbConfig{ + BasePath: testServer.URL, + UserId: "ur12345678", + }, + } + ratings, err := c.RatingsGet() + tt.assertions(assert.New(t), ratings, err) + }) + } +} diff --git a/pkg/client/testdata/imdb_list.csv b/pkg/client/testdata/imdb_list.csv new file mode 100644 index 0000000..3dfbca8 --- /dev/null +++ b/pkg/client/testdata/imdb_list.csv @@ -0,0 +1,4 @@ +Position,Const,Created,Modified,Description,Title,URL,Title Type,IMDb Rating,Runtime (mins),Year,Genres,Num Votes,Release Date,Directors,Your Rating,Date Rated +1,tt5013056,2023-08-03,2023-08-03,,Dunkirk,https://www.imdb.com/title/tt5013056/,movie,7.8,106,2017,"Action, Drama, History, Thriller, War",718267,2017-07-13,Christopher Nolan,8,2017-12-25 +2,tt15398776,2022-05-22,2022-05-22,,Oppenheimer,https://www.imdb.com/title/tt15398776/,movie,8.5,180,2023,"Biography, Drama, History",513747,2023-07-11,Christopher Nolan,6,2023-11-25 +3,tt0172495,2023-07-11,2023-07-11,,Gladiator,https://www.imdb.com/title/tt0172495/,movie,8.5,155,2000,"Action, Adventure, Drama",1577426,2000-05-01,Ridley Scott,10,2010-01-13 diff --git a/pkg/client/testdata/imdb_lists.html b/pkg/client/testdata/imdb_lists.html new file mode 100644 index 0000000..0cf52ef --- /dev/null +++ b/pkg/client/testdata/imdb_lists.html @@ -0,0 +1,8 @@ + diff --git a/pkg/client/testdata/imdb_ratings.csv b/pkg/client/testdata/imdb_ratings.csv new file mode 100644 index 0000000..047b5f3 --- /dev/null +++ b/pkg/client/testdata/imdb_ratings.csv @@ -0,0 +1,4 @@ +Const,Your Rating,Date Rated,Title,URL,Title Type,IMDb Rating,Runtime (mins),Year,Genres,Num Votes,Release Date,Directors +tt5013056,8,2017-12-25,Dunkirk,https://www.imdb.com/title/tt5013056/,movie,7.8,106,2017,"Action, Drama, History, Thriller, War",718267,2017-07-13,Christopher Nolan +tt15398776,6,2023-11-25,Oppenheimer,https://www.imdb.com/title/tt15398776/,movie,8.5,180,2023,"Biography, Drama, History",513747,2023-07-11,Christopher Nolan +tt0172495,10,2010-01-13,Gladiator,https://www.imdb.com/title/tt0172495/,movie,8.5,155,2000,"Action, Adventure, Drama",1577426,2000-05-01,Ridley Scott \ No newline at end of file diff --git a/pkg/logger/slog.go b/pkg/logger/slog.go index 4c25e60..bf0c1f0 100644 --- a/pkg/logger/slog.go +++ b/pkg/logger/slog.go @@ -1,18 +1,18 @@ package logger import ( + "io" "log/slog" - "os" ) const keyError = "error" -func NewLogger() *slog.Logger { +func NewLogger(writer io.Writer) *slog.Logger { opts := &slog.HandlerOptions{ AddSource: true, Level: slog.LevelInfo, } - handler := slog.NewJSONHandler(os.Stdout, opts) + handler := slog.NewJSONHandler(writer, opts) return slog.New(handler) } diff --git a/pkg/syncer/syncer.go b/pkg/syncer/syncer.go index 0622b78..5279a20 100644 --- a/pkg/syncer/syncer.go +++ b/pkg/syncer/syncer.go @@ -42,7 +42,7 @@ type user struct { func NewSyncer() *Syncer { syncer := &Syncer{ - logger: logger.NewLogger(), + logger: logger.NewLogger(os.Stdout), user: &user{ imdbLists: make(map[string]entities.ImdbList), imdbRatings: make(map[string]entities.ImdbItem),