From aa5362cb9c9c49c4a7a5a0dd858d093857afa722 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sat, 9 May 2020 21:23:57 +0200 Subject: [PATCH 01/25] Split models into separate repository files --- cmd/timecode_like_repository.go | 9 +++++++++ cmd/{models.go => timecode_repository.go} | 19 ++++--------------- cmd/user_repository.go | 12 ++++++++++++ 3 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 cmd/timecode_like_repository.go rename cmd/{models.go => timecode_repository.go} (51%) create mode 100644 cmd/user_repository.go diff --git a/cmd/timecode_like_repository.go b/cmd/timecode_like_repository.go new file mode 100644 index 0000000..902f03f --- /dev/null +++ b/cmd/timecode_like_repository.go @@ -0,0 +1,9 @@ +package main + +import "github.com/jinzhu/gorm" + +type TimecodeLike struct { + gorm.Model + TimecodeID uint `json:"timecodeId" gorm:"not null"` + UserID uint `json:"userId" gorm:"not null"` +} diff --git a/cmd/models.go b/cmd/timecode_repository.go similarity index 51% rename from cmd/models.go rename to cmd/timecode_repository.go index 5886583..d818205 100644 --- a/cmd/models.go +++ b/cmd/timecode_repository.go @@ -1,8 +1,10 @@ package main -import "github.com/jinzhu/gorm" +import ( + "github.com/jinzhu/gorm" +) -// Timecode struct +// Timecode represents timecode model type Timecode struct { gorm.Model Description string `json:"description"` @@ -10,16 +12,3 @@ type Timecode struct { VideoID string `json:"videoId" gorm:"not null;index"` Likes []TimecodeLike `json:"likes" gorm:"foreignkey:TimecodeID"` } - -type User struct { - gorm.Model - Email string - GoogleID string - PictureURL string -} - -type TimecodeLike struct { - gorm.Model - TimecodeID uint `json:"timecodeId" gorm:"not null"` - UserID uint `json:"userId" gorm:"not null"` -} diff --git a/cmd/user_repository.go b/cmd/user_repository.go new file mode 100644 index 0000000..c551ffe --- /dev/null +++ b/cmd/user_repository.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/jinzhu/gorm" +) + +type User struct { + gorm.Model + Email string + GoogleID string + PictureURL string +} From 62bd09bba4e41612c6913b8e9a2808e5a12b3259 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sun, 10 May 2020 12:12:28 +0200 Subject: [PATCH 02/25] Create UserRepository --- cmd/user_repository.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cmd/user_repository.go b/cmd/user_repository.go index c551ffe..19a11e9 100644 --- a/cmd/user_repository.go +++ b/cmd/user_repository.go @@ -1,6 +1,8 @@ package main import ( + googleAPI "timecodes/cmd/google_api" + "github.com/jinzhu/gorm" ) @@ -10,3 +12,21 @@ type User struct { GoogleID string PictureURL string } + +type UserRepository interface { + FindOrCreateByGoogleInfo(*googleAPI.UserInfo) *User +} + +type DBUserRepository struct { + DB *gorm.DB +} + +func (repo *DBUserRepository) FindOrCreateByGoogleInfo(userInfo *googleAPI.UserInfo) *User { + user := &User{} + + repo.DB.Where(User{GoogleID: userInfo.Id}). + Assign(User{Email: userInfo.Email, PictureURL: userInfo.Picture}). + FirstOrCreate(&user) + + return user +} From 78979d18e4306ef6a556eeaeae65fb44f8f18544 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sun, 10 May 2020 12:14:01 +0200 Subject: [PATCH 03/25] Create TimecodeRepository --- cmd/timecode_repository.go | 55 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/cmd/timecode_repository.go b/cmd/timecode_repository.go index d818205..a77f658 100644 --- a/cmd/timecode_repository.go +++ b/cmd/timecode_repository.go @@ -1,6 +1,8 @@ package main import ( + timecodeParser "timecodes/cmd/timecode_parser" + "github.com/jinzhu/gorm" ) @@ -12,3 +14,56 @@ type Timecode struct { VideoID string `json:"videoId" gorm:"not null;index"` Likes []TimecodeLike `json:"likes" gorm:"foreignkey:TimecodeID"` } + +type TimecodeRepository interface { + FindByVideoId(string) (*[]*Timecode, error) + Create(*Timecode) (*Timecode, error) + CreateFromParsedCodes([]timecodeParser.ParsedTimeCode, string) (*[]*Timecode, error) +} + +type DBTimecodeRepository struct { + DB *gorm.DB +} + +func (repo *DBTimecodeRepository) FindByVideoId(videoID string) (*[]*Timecode, error) { + timecodes := &[]*Timecode{} + + err := repo.DB.Order("seconds asc"). + Preload("Likes"). + Where(&Timecode{VideoID: videoID}). + Find(timecodes). + Error + + return timecodes, err +} + +func (repo *DBTimecodeRepository) Create(timecode *Timecode) (*Timecode, error) { + err := repo.DB.Create(timecode).Error + + return timecode, err +} + +func (repo *DBTimecodeRepository) CreateFromParsedCodes(parsedTimecodes []timecodeParser.ParsedTimeCode, videoId string) (*[]*Timecode, error) { + seen := make(map[string]struct{}) + + var collection []*Timecode + var err error + for _, code := range parsedTimecodes { + key := string(code.Seconds) + code.Description + if _, ok := seen[key]; ok { + continue + } + + seen[key] = struct{}{} + + timecode := &Timecode{Seconds: code.Seconds, VideoID: videoId, Description: code.Description} + + err = repo.DB.Create(timecode).Error + if err != nil { + continue + } + collection = append(collection, timecode) + } + + return &collection, err +} From 8f4e4bfaef64669082e355740d549d37e1711049 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sun, 10 May 2020 12:14:20 +0200 Subject: [PATCH 04/25] Create TimecodeLikeRepository --- cmd/timecode_like_repository.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/cmd/timecode_like_repository.go b/cmd/timecode_like_repository.go index 902f03f..2725b03 100644 --- a/cmd/timecode_like_repository.go +++ b/cmd/timecode_like_repository.go @@ -7,3 +7,31 @@ type TimecodeLike struct { TimecodeID uint `json:"timecodeId" gorm:"not null"` UserID uint `json:"userId" gorm:"not null"` } + +type TimecodeLikeRepository interface { + Create(*TimecodeLike, uint) (*TimecodeLike, error) + Delete(*TimecodeLike, uint) (*TimecodeLike, error) +} + +type DBTimecodeLikeRepository struct { + DB *gorm.DB +} + +func (repo *DBTimecodeLikeRepository) Create(timecodeLike *TimecodeLike, userID uint) (*TimecodeLike, error) { + timecodeLike.UserID = userID + + err := repo.DB.Create(timecodeLike).Error + + return timecodeLike, err +} + +func (repo *DBTimecodeLikeRepository) Delete(timecodeLike *TimecodeLike, userID uint) (*TimecodeLike, error) { + err := repo.DB.Where(&TimecodeLike{UserID: userID, TimecodeID: timecodeLike.TimecodeID}).First(timecodeLike).Error + if err != nil { + return nil, err + } + + err = repo.DB.Unscoped().Delete(timecodeLike).Error + + return timecodeLike, err +} From 559d56fb1cccdbeff2f7fb03ed01e48938bc4b25 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sun, 10 May 2020 12:17:04 +0200 Subject: [PATCH 05/25] Define Container for DI --- cmd/container.go | 13 +++++++++++++ cmd/db.go | 10 ++++++---- cmd/main.go | 28 +++++++++++----------------- 3 files changed, 30 insertions(+), 21 deletions(-) create mode 100644 cmd/container.go diff --git a/cmd/container.go b/cmd/container.go new file mode 100644 index 0000000..2253672 --- /dev/null +++ b/cmd/container.go @@ -0,0 +1,13 @@ +package main + +import ( + youtubeapi "timecodes/cmd/youtube_api" +) + +type Container struct { + TimecodeLikeRepository TimecodeLikeRepository + TimecodeRepository TimecodeRepository + UserRepository UserRepository + + YoutubeAPI *youtubeapi.Service +} diff --git a/cmd/db.go b/cmd/db.go index 3f40a38..d7e73d0 100644 --- a/cmd/db.go +++ b/cmd/db.go @@ -10,7 +10,7 @@ import ( _ "github.com/jinzhu/gorm/dialects/postgres" ) -func initDB() { +func initDB() *gorm.DB { var err error dsn := url.URL{ @@ -21,16 +21,18 @@ func initDB() { RawQuery: (&url.Values{"sslmode": []string{"disable"}}).Encode(), } - db, err = gorm.Open("postgres", dsn.String()) + db, err := gorm.Open("postgres", dsn.String()) if err != nil { log.Println("Failed to connect to database") panic(err) } log.Println("DB connection has been established.") + + return db } -func createTables() { +func createTables(db *gorm.DB) { if db.HasTable(&Timecode{}) { return } @@ -41,7 +43,7 @@ func createTables() { } } -func runMigrations() { +func runMigrations(db *gorm.DB) { db.AutoMigrate(&Timecode{}) db.Model(&Timecode{}).AddUniqueIndex( "idx_timecodes_seconds_text_video_id", diff --git a/cmd/main.go b/cmd/main.go index 7e9277c..e2f0f54 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,27 +1,21 @@ package main import ( - "github.com/jinzhu/gorm" - youtubeAPI "timecodes/cmd/youtube_api" ) -var ( - db *gorm.DB - youtubeService *youtubeAPI.Service -) +func main() { + db := initDB() + createTables(db) + runMigrations(db) -func init() { - initDB() - createTables() - runMigrations() - initYoutubeService() -} + container := &Container{ + UserRepository: &DBUserRepository{DB: db}, + TimecodeRepository: &DBTimecodeRepository{DB: db}, + TimecodeLikeRepository: &DBTimecodeLikeRepository{DB: db}, -func initYoutubeService() { - youtubeService = youtubeAPI.New() -} + YoutubeAPI: youtubeAPI.New(), + } -func main() { - startHttpServer() + startHttpServer(container) } From 18211a2061e5c3a9de21b3c39aad4faf0b7d29b6 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sun, 10 May 2020 12:17:22 +0200 Subject: [PATCH 06/25] Update auth middleware to accept container --- cmd/auth_middleware.go | 44 ++++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/cmd/auth_middleware.go b/cmd/auth_middleware.go index 8d224ac..bda029e 100644 --- a/cmd/auth_middleware.go +++ b/cmd/auth_middleware.go @@ -12,21 +12,25 @@ var authTokenRegExp = regexp.MustCompile(`Bearer (\S+$)`) type CurrentUserKey struct{} -func authMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token := getAuthToken(r.Header.Get("Authorization")) - - userInfo, err := googleAPI.FetchUserInfo(token) - if err != nil { - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - return - } - - ctx := context.WithValue(r.Context(), CurrentUserKey{}, findOrCreateUser(userInfo)) - r = r.WithContext(ctx) - - next.ServeHTTP(w, r) - }) +func authMiddleware(c *Container) (mw func(http.Handler) http.Handler) { + mw = func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := getAuthToken(r.Header.Get("Authorization")) + + userInfo, err := googleAPI.FetchUserInfo(token) + if err != nil { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + user := c.UserRepository.FindOrCreateByGoogleInfo(userInfo) + ctx := context.WithValue(r.Context(), CurrentUserKey{}, user) + r = r.WithContext(ctx) + + next.ServeHTTP(w, r) + }) + } + return } func getAuthToken(authorizationHeader string) string { @@ -40,13 +44,3 @@ func getAuthToken(authorizationHeader string) string { return string(token) } - -func findOrCreateUser(userInfo *googleAPI.UserInfo) *User { - currentUser := &User{} - - db.Where(User{GoogleID: userInfo.Id}). - Assign(User{Email: userInfo.Email, PictureURL: userInfo.Picture}). - FirstOrCreate(¤tUser) - - return currentUser -} From f4226b0326c2cc39d5fced455233692b57590290 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sun, 10 May 2020 12:18:19 +0200 Subject: [PATCH 07/25] Implement container-aware route handlers --- cmd/router.go | 24 +++++++++---- cmd/timecode_likes_controller.go | 23 +++++------- cmd/timecodes_controller.go | 61 ++++++++++---------------------- 3 files changed, 44 insertions(+), 64 deletions(-) diff --git a/cmd/router.go b/cmd/router.go index f6e03dc..13a1914 100644 --- a/cmd/router.go +++ b/cmd/router.go @@ -9,7 +9,17 @@ import ( "github.com/rs/cors" ) -func startHttpServer() { +type Handler struct { + *Container + H func(c *Container, w http.ResponseWriter, r *http.Request) +} + +// ServeHTTP allows our Handler type to satisfy http.Handler. +func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.H(h.Container, w, r) +} + +func startHttpServer(container *Container) { log.Println("Starting development server at http://127.0.0.1:8080/") router := mux.NewRouter().StrictSlash(true) @@ -17,19 +27,19 @@ func startHttpServer() { // public router.HandleFunc("/", handleHome) - router.HandleFunc("/timecodes/{videoId}", handleGetTimecodes) + router.Handle("/timecodes/{videoId}", Handler{container, handleGetTimecodes}) // auth auth := router.PathPrefix("/auth").Subrouter() - auth.Use(authMiddleware) + auth.Use(authMiddleware(container)) auth.HandleFunc("/login", handleLogin) - auth.HandleFunc("/timecodes", handleCreateTimecode).Methods(http.MethodPost) - auth.HandleFunc("/timecodes/{videoId}", handleGetTimecodes) + auth.Handle("/timecodes", Handler{container, handleCreateTimecode}).Methods(http.MethodPost) + auth.Handle("/timecodes/{videoId}", Handler{container, handleGetTimecodes}) - auth.HandleFunc("/timecode_likes", handleCreateTimecodeLike).Methods(http.MethodPost) - auth.HandleFunc("/timecode_likes", handleDeleteTimecodeLike).Methods(http.MethodDelete) + auth.Handle("/timecode_likes", Handler{container, handleCreateTimecodeLike}).Methods(http.MethodPost) + auth.Handle("/timecode_likes", Handler{container, handleDeleteTimecodeLike}).Methods(http.MethodDelete) handler := cors.Default().Handler(router) diff --git a/cmd/timecode_likes_controller.go b/cmd/timecode_likes_controller.go index 0bcf59c..16ec42d 100644 --- a/cmd/timecode_likes_controller.go +++ b/cmd/timecode_likes_controller.go @@ -8,9 +8,9 @@ import ( ) // POST /timecode_likes -func handleCreateTimecodeLike(w http.ResponseWriter, r *http.Request) { +func handleCreateTimecodeLike(c *Container, w http.ResponseWriter, r *http.Request) { currentUser := getCurrentUser(r) - like := &TimecodeLike{UserID: currentUser.ID} + like := &TimecodeLike{} reqBody, _ := ioutil.ReadAll(r.Body) err := json.Unmarshal(reqBody, like) @@ -19,7 +19,7 @@ func handleCreateTimecodeLike(w http.ResponseWriter, r *http.Request) { return } - err = db.Create(like).Error + _, err = c.TimecodeLikeRepository.Create(like, currentUser.ID) if err != nil { json.NewEncoder(w).Encode(err) @@ -29,28 +29,21 @@ func handleCreateTimecodeLike(w http.ResponseWriter, r *http.Request) { } // DELETE /timecode_likes -func handleDeleteTimecodeLike(w http.ResponseWriter, r *http.Request) { +func handleDeleteTimecodeLike(c *Container, w http.ResponseWriter, r *http.Request) { currentUser := getCurrentUser(r) - likeParams := &TimecodeLike{} - like := &TimecodeLike{} + timecodeLike := &TimecodeLike{} reqBody, _ := ioutil.ReadAll(r.Body) - err := json.Unmarshal(reqBody, likeParams) + err := json.Unmarshal(reqBody, timecodeLike) if err != nil { log.Println(err) return } - err = db.Where(&TimecodeLike{UserID: currentUser.ID, TimecodeID: likeParams.TimecodeID}).First(like).Error - if err != nil { - json.NewEncoder(w).Encode(err) - return - } - - err = db.Unscoped().Delete(like).Error + _, err = c.TimecodeLikeRepository.Delete(timecodeLike, currentUser.ID) if err != nil { json.NewEncoder(w).Encode(err) } else { - json.NewEncoder(w).Encode(like) + json.NewEncoder(w).Encode(timecodeLike) } } diff --git a/cmd/timecodes_controller.go b/cmd/timecodes_controller.go index 3dd010b..80c2920 100644 --- a/cmd/timecodes_controller.go +++ b/cmd/timecodes_controller.go @@ -21,24 +21,18 @@ type TimecodeJSON struct { } // GET /timecodes -func handleGetTimecodes(w http.ResponseWriter, r *http.Request) { +func handleGetTimecodes(c *Container, w http.ResponseWriter, r *http.Request) { currentUser := getCurrentUser(r) - timecodes := &[]*Timecode{} - videoId := mux.Vars(r)["videoId"] - - err := db.Order("seconds asc"). - Preload("Likes"). - Where(&Timecode{VideoID: videoId}). - Find(timecodes). - Error + videoID := mux.Vars(r)["videoId"] + timecodes, err := c.TimecodeRepository.FindByVideoId(videoID) if err != nil { json.NewEncoder(w).Encode(err) } else { if len(*timecodes) == 0 { go func() { - parseDescriptionAndCreateAnnotations(videoId) - parseCommentsAndCreateAnnotations(videoId) + parseDescriptionAndCreateAnnotations(c, videoID) + parseCommentsAndCreateAnnotations(c, videoID) }() } @@ -52,17 +46,13 @@ func handleGetTimecodes(w http.ResponseWriter, r *http.Request) { } // POST /timecodes -func handleCreateTimecode(w http.ResponseWriter, r *http.Request) { +func handleCreateTimecode(c *Container, w http.ResponseWriter, r *http.Request) { currentUser := getCurrentUser(r) timecode := &Timecode{} reqBody, _ := ioutil.ReadAll(r.Body) - err := json.Unmarshal(reqBody, timecode) - if err != nil { - json.NewEncoder(w).Encode(err) - return - } - err = db.Create(timecode).Error + json.Unmarshal(reqBody, timecode) + _, err := c.TimecodeRepository.Create(timecode) if err != nil { json.NewEncoder(w).Encode(err) @@ -97,17 +87,20 @@ func getLikedByMe(likes []TimecodeLike, userID uint) bool { return false } -func parseDescriptionAndCreateAnnotations(videoId string) { - description := youtubeService.FetchVideoDescription(videoId) +func parseDescriptionAndCreateAnnotations(c *Container, videoID string) { + description := c.YoutubeAPI.FetchVideoDescription(videoID) parsedCodes := timecodeParser.Parse(description) - createTimecodes(parsedCodes, videoId) + _, err := c.TimecodeRepository.CreateFromParsedCodes(parsedCodes, videoID) + if err != nil { + log.Println(err) + } } -func parseCommentsAndCreateAnnotations(videoId string) { +func parseCommentsAndCreateAnnotations(c *Container, videoID string) { var parsedCodes []timecodeParser.ParsedTimeCode - comments, err := youtubeService.FetchVideoComments(videoId) + comments, err := c.YoutubeAPI.FetchVideoComments(videoID) if err != nil { log.Println(err) @@ -120,24 +113,8 @@ func parseCommentsAndCreateAnnotations(videoId string) { parsedCodes = append(parsedCodes, timeCodes...) } - createTimecodes(parsedCodes, videoId) -} - -func createTimecodes(parsedTimecodes []timecodeParser.ParsedTimeCode, videoId string) { - seen := make(map[string]struct{}) - - for _, code := range parsedTimecodes { - key := string(code.Seconds) + code.Description - if _, ok := seen[key]; ok { - continue - } - - seen[key] = struct{}{} - - annotation := &Timecode{Seconds: code.Seconds, VideoID: videoId, Description: code.Description} - err := db.Create(annotation).Error - if err != nil { - log.Println(err) - } + _, err = c.TimecodeRepository.CreateFromParsedCodes(parsedCodes, videoID) + if err != nil { + log.Println(err) } } From ba5b133341b912fbebd91f48e8d77839d36d452b Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sun, 10 May 2020 14:00:25 +0200 Subject: [PATCH 08/25] Add unit tests for google_api#FetchUserInfo --- cmd/google_api/google_api.go | 16 +++- cmd/google_api/google_api_test.go | 119 ++++++++++++++++++++++++++++++ cmd/user_repository.go | 2 +- go.mod | 1 + go.sum | 2 + 5 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 cmd/google_api/google_api_test.go diff --git a/cmd/google_api/google_api.go b/cmd/google_api/google_api.go index c6f4275..ce00d2f 100644 --- a/cmd/google_api/google_api.go +++ b/cmd/google_api/google_api.go @@ -11,11 +11,21 @@ import ( const userInfoHost = "https://www.googleapis.com/oauth2/v2/userinfo" type UserInfo struct { - Id string `json:"id"` + ID string `json:"id"` Picture string `json:"picture"` Email string `json:"email"` } +type APIError struct { + ErrorData struct { + Message string `json:"message"` + } `json:"error"` +} + +func (e *APIError) Error() string { + return e.ErrorData.Message +} + func FetchUserInfo(accessToken string) (userInfo *UserInfo, err error) { if len(accessToken) == 0 { return nil, errors.New("accessToken cannot be empty") @@ -35,7 +45,9 @@ func FetchUserInfo(accessToken string) (userInfo *UserInfo, err error) { switch response.StatusCode { case 401: - userInfo, err = nil, errors.New(string(contents)) + apiError := &APIError{} + json.Unmarshal(contents, apiError) + userInfo, err = nil, apiError default: userInfo = &UserInfo{} err = nil diff --git a/cmd/google_api/google_api_test.go b/cmd/google_api/google_api_test.go new file mode 100644 index 0000000..c92ad04 --- /dev/null +++ b/cmd/google_api/google_api_test.go @@ -0,0 +1,119 @@ +package googleapi + +import ( + "errors" + "net/http" + "os" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" +) + +type errReader int + +func (errReader) Read(p []byte) (n int, err error) { + return 0, errors.New("reader error") +} + +func (errReader) Close() error { + return nil +} + +func TestMain(m *testing.M) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + registerMockResponders() + + os.Exit(m.Run()) +} + +func registerMockResponders() { + httpmock.RegisterResponder( + "GET", "https://www.googleapis.com/oauth2/v2/userinfo?access_token=tokenWithInvalidData", + httpmock.ResponderFromResponse( + &http.Response{ + Status: "200", + StatusCode: 200, + Body: errReader(0), + Header: http.Header{}, + ContentLength: 1, + }, + ), + ) + + unauthorizedError := ` + { + "error": { + "code": 401, + "message": "Request is missing required authentication credential.", + "status": "UNAUTHENTICATED" + } + } + ` + httpmock.RegisterResponder( + "GET", "https://www.googleapis.com/oauth2/v2/userinfo?access_token=invalidToken", + httpmock.NewStringResponder(401, unauthorizedError), + ) + + userInfoResponse := ` + { + "id": "92752", + "email": "better_call_your_local_dealer@gmail.com", + "verified_email": true, + "picture": "https://lh3.googleusercontent.com/a-/0f78ce08-876e-4ffa-a227-84f52a69063f" + } + ` + httpmock.RegisterResponder( + "GET", "https://www.googleapis.com/oauth2/v2/userinfo?access_token=validToken", + httpmock.NewStringResponder(200, userInfoResponse), + ) +} + +func TestFetchUserInfo(t *testing.T) { + t.Run("when given accessToken is empty", func(t *testing.T) { + token := "" + userInfo, err := FetchUserInfo(token) + + assert.Nil(t, userInfo) + assert.EqualError(t, err, "accessToken cannot be empty") + }) + + t.Run("when http client error occurs", func(t *testing.T) { + token := "tokenWithClientError" + userInfo, err := FetchUserInfo(token) + + assert.Nil(t, userInfo) + assert.EqualError( + t, + err, + `failed getting user info: Get "https://www.googleapis.com/oauth2/v2/userinfo?access_token=tokenWithClientError": no responder found`, + ) + }) + + t.Run("when response body contains invalid data", func(t *testing.T) { + token := "tokenWithInvalidData" + userInfo, err := FetchUserInfo(token) + + assert.Nil(t, userInfo) + assert.EqualError(t, err, "failed reading response body: reader error") + }) + + t.Run("when given accessToken return unauthorized response", func(t *testing.T) { + token := "invalidToken" + userInfo, err := FetchUserInfo(token) + + assert.Nil(t, userInfo) + assert.EqualError(t, err, "Request is missing required authentication credential.") + }) + + t.Run("when valid access token given", func(t *testing.T) { + token := "validToken" + userInfo, err := FetchUserInfo(token) + + assert.NotNil(t, userInfo) + assert.Equal(t, "92752", userInfo.ID) + assert.Nil(t, err) + }) +} diff --git a/cmd/user_repository.go b/cmd/user_repository.go index 19a11e9..994eb92 100644 --- a/cmd/user_repository.go +++ b/cmd/user_repository.go @@ -24,7 +24,7 @@ type DBUserRepository struct { func (repo *DBUserRepository) FindOrCreateByGoogleInfo(userInfo *googleAPI.UserInfo) *User { user := &User{} - repo.DB.Where(User{GoogleID: userInfo.Id}). + repo.DB.Where(User{GoogleID: userInfo.ID}). Assign(User{Email: userInfo.Email, PictureURL: userInfo.Picture}). FirstOrCreate(&user) diff --git a/go.mod b/go.mod index 7668293..bfb22f0 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/gorilla/mux v1.7.4 + github.com/jarcoal/httpmock v1.0.5 github.com/jinzhu/gorm v1.9.12 github.com/rs/cors v1.7.0 github.com/stretchr/testify v1.5.1 diff --git a/go.sum b/go.sum index 765424b..d236f1f 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/jarcoal/httpmock v1.0.5 h1:cHtVEcTxRSX4J0je7mWPfc9BpDpqzXSJ5HbymZmyHck= +github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= From 768410e4ebabbfc544f18eaf2f6643db5d63279b Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sun, 10 May 2020 18:52:15 +0200 Subject: [PATCH 09/25] Add unit tests for youtube_api package --- cmd/main.go | 10 +- cmd/youtube_api/youtube_api.go | 22 +++-- cmd/youtube_api/youtube_api_test.go | 146 ++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 cmd/youtube_api/youtube_api_test.go diff --git a/cmd/main.go b/cmd/main.go index e2f0f54..7259516 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,6 +1,8 @@ package main import ( + "log" + youtubeAPI "timecodes/cmd/youtube_api" ) @@ -9,12 +11,16 @@ func main() { createTables(db) runMigrations(db) + youtubeAPI, err := youtubeAPI.New() + if err != nil { + log.Fatal(err) + } + container := &Container{ UserRepository: &DBUserRepository{DB: db}, TimecodeRepository: &DBTimecodeRepository{DB: db}, TimecodeLikeRepository: &DBTimecodeLikeRepository{DB: db}, - - YoutubeAPI: youtubeAPI.New(), + YoutubeAPI: youtubeAPI, } startHttpServer(container) diff --git a/cmd/youtube_api/youtube_api.go b/cmd/youtube_api/youtube_api.go index 8fcf0f3..39b7f19 100644 --- a/cmd/youtube_api/youtube_api.go +++ b/cmd/youtube_api/youtube_api.go @@ -9,17 +9,19 @@ import ( "google.golang.org/api/youtube/v3" ) +const GOOGLE_API_KEY = "GOOGLE_API_KEY" + type Service struct { client *youtube.Service } -func New() *Service { - youtubeService, err := youtube.NewService(context.Background(), option.WithAPIKey(os.Getenv("GOOGLE_API_KEY"))) +func New() (*Service, error) { + youtubeService, err := youtube.NewService(context.Background(), option.WithAPIKey(os.Getenv(GOOGLE_API_KEY))) if err != nil { - log.Println(err) + return nil, err } - return &Service{client: youtubeService} + return &Service{client: youtubeService}, nil } func (s *Service) FetchVideoDescription(videoId string) string { @@ -43,7 +45,9 @@ func (s *Service) FetchVideoDescription(videoId string) string { return items[0].Snippet.Description } -func (s *Service) FetchVideoComments(videoId string) ([]*youtube.CommentThread, error) { +func (s *Service) FetchVideoComments(videoId string) []string { + textComments := make([]string, 0) + call := s.client. CommentThreads. List("snippet"). @@ -55,8 +59,12 @@ func (s *Service) FetchVideoComments(videoId string) ([]*youtube.CommentThread, if err != nil { log.Println(err) - return nil, err + return textComments + } + + for _, item := range response.Items { + textComments = append(textComments, item.Snippet.TopLevelComment.Snippet.TextOriginal) } - return response.Items, nil + return textComments } diff --git a/cmd/youtube_api/youtube_api_test.go b/cmd/youtube_api/youtube_api_test.go new file mode 100644 index 0000000..72f1565 --- /dev/null +++ b/cmd/youtube_api/youtube_api_test.go @@ -0,0 +1,146 @@ +package youtubeapi + +import ( + "os" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" +) + +func TestMain(m *testing.M) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + registerMockResponders() + + os.Exit(m.Run()) +} + +func registerMockResponders() { + legitVideoResponse := ` + { + "items": [ + { + "id": "legitVideoId", + "snippet": { + "description": "Legit description" + } + } + ] + } + ` + httpmock.RegisterResponder( + "GET", "https://www.googleapis.com/youtube/v3/videos?alt=json&id=legitVideoId&key=CORRECT_KEY&part=snippet&prettyPrint=false", + httpmock.NewStringResponder(200, legitVideoResponse), + ) + + notExistingVideoResponse := `{ "items": [] }` + httpmock.RegisterResponder( + "GET", "https://www.googleapis.com/youtube/v3/videos?alt=json&id=notExistingVideo&key=CORRECT_KEY&part=snippet&prettyPrint=false", + httpmock.NewStringResponder(200, notExistingVideoResponse), + ) + + httpmock.RegisterResponder( + "GET", "https://www.googleapis.com/youtube/v3/videos?alt=json&id=wrongResponse&key=CORRECT_KEY&part=snippet&prettyPrint=false", + httpmock.NewStringResponder(500, "Something went wrong :("), + ) + + legitCommentsResponse := ` + { + "items": [ + { + "snippet": { + "topLevelComment": { + "snippet": { + "textOriginal": "Just a comment." + } + } + } + } + ] + } + ` + httpmock.RegisterResponder( + "GET", "https://www.googleapis.com/youtube/v3/commentThreads?alt=json&key=CORRECT_KEY&maxResults=100&order=relevance&part=snippet&prettyPrint=false&videoId=legitVideoId", + httpmock.NewStringResponder(200, legitCommentsResponse), + ) +} + +func TestNew(t *testing.T) { + t.Run("when youtube.NewService returns an error", func(t *testing.T) { + os.Setenv(GOOGLE_API_KEY, "") + defer os.Unsetenv(GOOGLE_API_KEY) + + service, err := New() + + assert.Nil(t, service) + assert.Contains(t, err.Error(), "google: could not find default credentials") + }) + + t.Run("when returns correct service", func(t *testing.T) { + os.Setenv(GOOGLE_API_KEY, "CORRECT_KEY") + defer os.Unsetenv(GOOGLE_API_KEY) + + service, err := New() + + assert.NotNil(t, service) + assert.Equal(t, "https://www.googleapis.com/youtube/v3/", service.client.BasePath) + assert.Nil(t, err) + }) +} + +func TestService_FetchVideoDescription(t *testing.T) { + os.Setenv(GOOGLE_API_KEY, "CORRECT_KEY") + defer os.Unsetenv(GOOGLE_API_KEY) + + t.Run("when video exists", func(t *testing.T) { + service, _ := New() + + videoID := "legitVideoId" + description := service.FetchVideoDescription(videoID) + + assert.Equal(t, "Legit description", description) + }) + + t.Run("when video doesn't exist", func(t *testing.T) { + service, _ := New() + + videoID := "notExistingVideo" + description := service.FetchVideoDescription(videoID) + + assert.Equal(t, "", description) + }) + + t.Run("when http client returns an error", func(t *testing.T) { + service, _ := New() + + videoID := "wrongResponse" + description := service.FetchVideoDescription(videoID) + + assert.Equal(t, "", description) + }) +} + +func TestService_FetchVideoComments(t *testing.T) { + os.Setenv(GOOGLE_API_KEY, "CORRECT_KEY") + defer os.Unsetenv(GOOGLE_API_KEY) + + t.Run("when video exists", func(t *testing.T) { + service, _ := New() + + videoID := "legitVideoId" + comments := service.FetchVideoComments(videoID) + + assert.Equal(t, "Just a comment.", comments[0]) + }) + + t.Run("when client returns an error", func(t *testing.T) { + service, _ := New() + + videoID := "wrongResponse" + comments := service.FetchVideoComments(videoID) + + assert.Empty(t, comments) + }) +} From 34c004170d850e32e41c83c08bb8df3c350eaccc Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sun, 10 May 2020 18:54:08 +0200 Subject: [PATCH 10/25] Refactor go routine for creating timecodes from yt video --- cmd/timecode_repository.go | 3 ++- cmd/timecodes_controller.go | 27 +++++---------------------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/cmd/timecode_repository.go b/cmd/timecode_repository.go index a77f658..09b3582 100644 --- a/cmd/timecode_repository.go +++ b/cmd/timecode_repository.go @@ -1,6 +1,7 @@ package main import ( + "strconv" timecodeParser "timecodes/cmd/timecode_parser" "github.com/jinzhu/gorm" @@ -49,7 +50,7 @@ func (repo *DBTimecodeRepository) CreateFromParsedCodes(parsedTimecodes []timeco var collection []*Timecode var err error for _, code := range parsedTimecodes { - key := string(code.Seconds) + code.Description + key := strconv.Itoa(code.Seconds) + code.Description if _, ok := seen[key]; ok { continue } diff --git a/cmd/timecodes_controller.go b/cmd/timecodes_controller.go index 80c2920..7989861 100644 --- a/cmd/timecodes_controller.go +++ b/cmd/timecodes_controller.go @@ -30,10 +30,7 @@ func handleGetTimecodes(c *Container, w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(err) } else { if len(*timecodes) == 0 { - go func() { - parseDescriptionAndCreateAnnotations(c, videoID) - parseCommentsAndCreateAnnotations(c, videoID) - }() + go parseVideoContentAndCreateTimecodes(c, videoID) } timecodeJSONCollection := make([]*TimecodeJSON, 0) @@ -87,33 +84,19 @@ func getLikedByMe(likes []TimecodeLike, userID uint) bool { return false } -func parseDescriptionAndCreateAnnotations(c *Container, videoID string) { +func parseVideoContentAndCreateTimecodes(c *Container, videoID string) { description := c.YoutubeAPI.FetchVideoDescription(videoID) parsedCodes := timecodeParser.Parse(description) - _, err := c.TimecodeRepository.CreateFromParsedCodes(parsedCodes, videoID) - if err != nil { - log.Println(err) - } -} - -func parseCommentsAndCreateAnnotations(c *Container, videoID string) { - var parsedCodes []timecodeParser.ParsedTimeCode - - comments, err := c.YoutubeAPI.FetchVideoComments(videoID) - if err != nil { - log.Println(err) - - return - } + comments := c.YoutubeAPI.FetchVideoComments(videoID) for _, comment := range comments { - timeCodes := timecodeParser.Parse(comment.Snippet.TopLevelComment.Snippet.TextOriginal) + timeCodes := timecodeParser.Parse(comment) parsedCodes = append(parsedCodes, timeCodes...) } - _, err = c.TimecodeRepository.CreateFromParsedCodes(parsedCodes, videoID) + _, err := c.TimecodeRepository.CreateFromParsedCodes(parsedCodes, videoID) if err != nil { log.Println(err) } From da29b9aa14e56f195abc97295943f02da53c124e Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sun, 10 May 2020 21:40:56 +0200 Subject: [PATCH 11/25] Set up test database --- .env.test | 8 ++++++ cmd/db.go | 70 ++++++++++++++++++++++++++++++++++------------ cmd/main.go | 1 - docker-compose.yml | 15 ++++++++++ go.mod | 1 + go.sum | 2 ++ 6 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 .env.test diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..ee3f804 --- /dev/null +++ b/.env.test @@ -0,0 +1,8 @@ +PORT=8080 +PG_USER=postgres +PG_PASSWORD=postgres +PG_HOST=db +PG_PORT=5432 +PG_DB=timecodes_db_test + +GOOGLE_API_KEY= diff --git a/cmd/db.go b/cmd/db.go index d7e73d0..c6eb1e2 100644 --- a/cmd/db.go +++ b/cmd/db.go @@ -6,40 +6,59 @@ import ( "net/url" "os" + "github.com/DATA-DOG/go-txdb" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/postgres" ) +const TXDB_NAME = "txdb_postgres" +const TXDB_CONNECTION_POOL_NAME = "timecodes_connection_pool" + func initDB() *gorm.DB { - var err error + envDsn := getDsn(os.Getenv("PG_DB")) - dsn := url.URL{ - User: url.UserPassword(os.Getenv("PG_USER"), os.Getenv("PG_PASSWORD")), - Scheme: "postgres", - Host: fmt.Sprintf("%s:%s", os.Getenv("PG_HOST"), os.Getenv("PG_PORT")), - Path: os.Getenv("PG_DB"), - RawQuery: (&url.Values{"sslmode": []string{"disable"}}).Encode(), + db, err := gorm.Open("postgres", envDsn.String()) + if err != nil { + db = createDatabase(os.Getenv("PG_DB")) } - db, err := gorm.Open("postgres", dsn.String()) + return db +} + +func createDatabase(dbName string) *gorm.DB { + defaultDsn := getDsn("postgres") + db, err := gorm.Open("postgres", defaultDsn.String()) + if err != nil { + handleDBConnectionError(err) + } + + db = db.Exec(fmt.Sprintf("CREATE DATABASE %s;", dbName)) if err != nil { - log.Println("Failed to connect to database") - panic(err) + handleDBConnectionError(err) } - log.Println("DB connection has been established.") + dsn := getDsn(dbName) + + db, err = gorm.Open("postgres", dsn.String()) + if err != nil { + handleDBConnectionError(err) + } return db } -func createTables(db *gorm.DB) { - if db.HasTable(&Timecode{}) { - return - } +func handleDBConnectionError(err error) { + log.Println("Unable to connect to db") + panic(err) +} - err := db.CreateTable(&Timecode{}) - if err != nil { - log.Println("Table already exists") +func getDsn(path string) url.URL { + return url.URL{ + User: url.UserPassword(os.Getenv("PG_USER"), os.Getenv("PG_PASSWORD")), + Scheme: "postgres", + Host: fmt.Sprintf("%s:%s", os.Getenv("PG_HOST"), os.Getenv("PG_PORT")), + Path: path, + RawQuery: (&url.Values{"sslmode": []string{"disable"}}).Encode(), } } @@ -56,3 +75,18 @@ func runMigrations(db *gorm.DB) { "user_id", "timecode_id", ) } + +func setupTestDB() *gorm.DB { + initDB() + dsn := getDsn(os.Getenv("PG_DB")) + txdb.Register(TXDB_NAME, "postgres", dsn.String()) + + db, err := gorm.Open("postgres", TXDB_NAME, TXDB_CONNECTION_POOL_NAME) + if err != nil { + handleDBConnectionError(err) + } + + runMigrations(db) + + return db +} diff --git a/cmd/main.go b/cmd/main.go index 7259516..8b364f6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -8,7 +8,6 @@ import ( func main() { db := initDB() - createTables(db) runMigrations(db) youtubeAPI, err := youtubeAPI.New() diff --git a/docker-compose.yml b/docker-compose.yml index e48efd5..74f298d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,21 @@ version: "3.5" services: + app_test: + build: + context: . + dockerfile: Dockerfile + target: builder + depends_on: + - db + environment: + - CGO_ENABLED=0 + env_file: + - .env.test + ports: + - "8080:8080" + volumes: + - .:/usr/src/app app_dev: build: context: . diff --git a/go.mod b/go.mod index bfb22f0..3b3768a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module timecodes go 1.13 require ( + github.com/DATA-DOG/go-txdb v0.1.3 github.com/gorilla/mux v1.7.4 github.com/jarcoal/httpmock v1.0.5 github.com/jinzhu/gorm v1.9.12 diff --git a/go.sum b/go.sum index d236f1f..c244889 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DATA-DOG/go-txdb v0.1.3 h1:R4v6OuOcy2O147e2zHxU0B4NDtF+INb5R9q/CV7AEMg= +github.com/DATA-DOG/go-txdb v0.1.3/go.mod h1:DhAhxMXZpUJVGnT+p9IbzJoRKvlArO2pkHjnGX7o0n0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= From 268d4a510f2773b9a7167f169095535e071563e4 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sun, 10 May 2020 21:41:21 +0200 Subject: [PATCH 12/25] Add tests for UserRepository --- cmd/user_repository.go | 11 +++++++--- cmd/user_repository_test.go | 41 +++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 cmd/user_repository_test.go diff --git a/cmd/user_repository.go b/cmd/user_repository.go index 994eb92..822918f 100644 --- a/cmd/user_repository.go +++ b/cmd/user_repository.go @@ -1,6 +1,7 @@ package main import ( + "fmt" googleAPI "timecodes/cmd/google_api" "github.com/jinzhu/gorm" @@ -24,9 +25,13 @@ type DBUserRepository struct { func (repo *DBUserRepository) FindOrCreateByGoogleInfo(userInfo *googleAPI.UserInfo) *User { user := &User{} - repo.DB.Where(User{GoogleID: userInfo.ID}). + err := repo.DB.Where(User{GoogleID: userInfo.ID}). Assign(User{Email: userInfo.Email, PictureURL: userInfo.Picture}). - FirstOrCreate(&user) - + FirstOrCreate(&user).Error + if err != nil { + fmt.Println("ERR") + fmt.Println(err) + } + fmt.Println(user) return user } diff --git a/cmd/user_repository_test.go b/cmd/user_repository_test.go new file mode 100644 index 0000000..88a7103 --- /dev/null +++ b/cmd/user_repository_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "testing" + + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/postgres" + "github.com/stretchr/testify/assert" + + googleAPI "timecodes/cmd/google_api" +) + +var TestDB *gorm.DB + +func TestDBUserRepository_FindOrCreateByGoogleInfo(t *testing.T) { + TestDB = setupTestDB() + defer TestDB.Close() + + t.Run("when user doesn't exist", func(t *testing.T) { + googleID := "10001" + repo := DBUserRepository{DB: TestDB} + userInfo := &googleAPI.UserInfo{ID: googleID} + + user := repo.FindOrCreateByGoogleInfo(userInfo) + + assert.Equal(t, "10001", user.GoogleID) + }) + + t.Run("when user exists", func(t *testing.T) { + googleID := "10002" + + TestDB.Create(&User{GoogleID: googleID}) + + repo := DBUserRepository{DB: TestDB} + userInfo := &googleAPI.UserInfo{ID: googleID} + + user := repo.FindOrCreateByGoogleInfo(userInfo) + + assert.Equal(t, "10002", user.GoogleID) + }) +} From b96747417e36c2ce048999da7c8309409a5e05be Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sun, 10 May 2020 21:41:29 +0200 Subject: [PATCH 13/25] Update README --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 5d0ed69..49619a4 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,20 @@ $ docker-compose up --build app_dev ``` +# Run the whole test suite + +```bash +$ docker-compose build app_test +$ docker-compose run app_test go test ./... -cover +``` + +# Run a single test + +```bash +$ docker-compose build app_test +$ docker-compose run app_test go test -run Test%TEST_FUNCTION_NAME% +``` + # Build for using in production ```bash From 7673d8fbb6c00c052cc754443b94c06dbce93658 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sun, 10 May 2020 21:51:49 +0200 Subject: [PATCH 14/25] Use docker-compose for tests --- .github/workflows/pr_checks.yml | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 886dd53..88d4ea7 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -15,17 +15,11 @@ jobs: steps: - name: Checkout current branch uses: actions/checkout@master - - name: Cache - uses: actions/cache@v1 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - uses: actions/setup-go@v1 - with: - go-version: '1.13' - - name: Run go test + - name: docker-compose version + run: docker-compose --version + - name: Build docker image run: | - go mod download - go test ./... -cover + cp .env.example .env + docker-compose build app_test + - name: Run tests + run: docker-compose run app_test go test ./... -cover From 4bf580ce6e44592cf61bea3edc0bb99a85a52d21 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sun, 10 May 2020 22:00:36 +0200 Subject: [PATCH 15/25] Fix linter errors --- cmd/db.go | 2 +- cmd/google_api/google_api.go | 8 ++++++-- cmd/timecodes_controller.go | 9 ++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/cmd/db.go b/cmd/db.go index c6eb1e2..55c9bf7 100644 --- a/cmd/db.go +++ b/cmd/db.go @@ -32,7 +32,7 @@ func createDatabase(dbName string) *gorm.DB { handleDBConnectionError(err) } - db = db.Exec(fmt.Sprintf("CREATE DATABASE %s;", dbName)) + err = db.Exec(fmt.Sprintf("CREATE DATABASE %s;", dbName)).Error if err != nil { handleDBConnectionError(err) } diff --git a/cmd/google_api/google_api.go b/cmd/google_api/google_api.go index ce00d2f..f5d68a8 100644 --- a/cmd/google_api/google_api.go +++ b/cmd/google_api/google_api.go @@ -46,8 +46,12 @@ func FetchUserInfo(accessToken string) (userInfo *UserInfo, err error) { switch response.StatusCode { case 401: apiError := &APIError{} - json.Unmarshal(contents, apiError) - userInfo, err = nil, apiError + unmarshalErr := json.Unmarshal(contents, apiError) + if unmarshalErr != nil { + return nil, unmarshalErr + } + + err = apiError default: userInfo = &UserInfo{} err = nil diff --git a/cmd/timecodes_controller.go b/cmd/timecodes_controller.go index 7989861..7b85c9c 100644 --- a/cmd/timecodes_controller.go +++ b/cmd/timecodes_controller.go @@ -48,9 +48,12 @@ func handleCreateTimecode(c *Container, w http.ResponseWriter, r *http.Request) timecode := &Timecode{} reqBody, _ := ioutil.ReadAll(r.Body) - json.Unmarshal(reqBody, timecode) - _, err := c.TimecodeRepository.Create(timecode) - + err := json.Unmarshal(reqBody, timecode) + if err != nil { + json.NewEncoder(w).Encode(err) + return + } + _, err = c.TimecodeRepository.Create(timecode) if err != nil { json.NewEncoder(w).Encode(err) } else { From c6a151f9ce4f55b72f417b932cb6979e8a52b906 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Mon, 11 May 2020 19:53:53 +0200 Subject: [PATCH 16/25] Remove debug prints --- cmd/user_repository.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/cmd/user_repository.go b/cmd/user_repository.go index 822918f..8bfecfd 100644 --- a/cmd/user_repository.go +++ b/cmd/user_repository.go @@ -1,7 +1,6 @@ package main import ( - "fmt" googleAPI "timecodes/cmd/google_api" "github.com/jinzhu/gorm" @@ -19,19 +18,17 @@ type UserRepository interface { } type DBUserRepository struct { + UserRepository + DB *gorm.DB } func (repo *DBUserRepository) FindOrCreateByGoogleInfo(userInfo *googleAPI.UserInfo) *User { user := &User{} - err := repo.DB.Where(User{GoogleID: userInfo.ID}). + repo.DB.Where(User{GoogleID: userInfo.ID}). Assign(User{Email: userInfo.Email, PictureURL: userInfo.Picture}). - FirstOrCreate(&user).Error - if err != nil { - fmt.Println("ERR") - fmt.Println(err) - } - fmt.Println(user) + FirstOrCreate(&user) + return user } From 984db8b0b72624dc2b01689aaa8ccc9a6bed361f Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Tue, 12 May 2020 21:42:02 +0200 Subject: [PATCH 17/25] Register test db only once --- cmd/db.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/db.go b/cmd/db.go index 55c9bf7..c1506bd 100644 --- a/cmd/db.go +++ b/cmd/db.go @@ -5,6 +5,7 @@ import ( "log" "net/url" "os" + "sync" "github.com/DATA-DOG/go-txdb" "github.com/jinzhu/gorm" @@ -14,6 +15,8 @@ import ( const TXDB_NAME = "txdb_postgres" const TXDB_CONNECTION_POOL_NAME = "timecodes_connection_pool" +var doOnce sync.Once + func initDB() *gorm.DB { envDsn := getDsn(os.Getenv("PG_DB")) @@ -79,7 +82,9 @@ func runMigrations(db *gorm.DB) { func setupTestDB() *gorm.DB { initDB() dsn := getDsn(os.Getenv("PG_DB")) - txdb.Register(TXDB_NAME, "postgres", dsn.String()) + doOnce.Do(func() { + txdb.Register(TXDB_NAME, "postgres", dsn.String()) + }) db, err := gorm.Open("postgres", TXDB_NAME, TXDB_CONNECTION_POOL_NAME) if err != nil { From 787d8d0adc8e07fa0d0ba48b19b5a0b51f930a27 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Tue, 12 May 2020 21:43:18 +0200 Subject: [PATCH 18/25] Use suite in UserRepository tests --- cmd/user_repository_test.go | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/cmd/user_repository_test.go b/cmd/user_repository_test.go index 88a7103..2d0509c 100644 --- a/cmd/user_repository_test.go +++ b/cmd/user_repository_test.go @@ -4,24 +4,40 @@ import ( "testing" "github.com/jinzhu/gorm" - _ "github.com/jinzhu/gorm/dialects/postgres" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" googleAPI "timecodes/cmd/google_api" ) -var TestDB *gorm.DB +type UserRepositorySuite struct { + suite.Suite + DB *gorm.DB + Repo *DBUserRepository +} + +func (suite *UserRepositorySuite) ResetDB() { + suite.DB.Exec("TRUNCATE TABLE users;") +} + +func (suite *UserRepositorySuite) SetupTest() { + db := setupTestDB() + suite.DB = db + suite.Repo = &DBUserRepository{DB: db} +} + +func TestUserRepositorySuite(t *testing.T) { + suite.Run(t, new(UserRepositorySuite)) +} -func TestDBUserRepository_FindOrCreateByGoogleInfo(t *testing.T) { - TestDB = setupTestDB() - defer TestDB.Close() +func (suite *UserRepositorySuite) TestDBUserRepository_FindOrCreateByGoogleInfo() { + t := suite.T() t.Run("when user doesn't exist", func(t *testing.T) { googleID := "10001" - repo := DBUserRepository{DB: TestDB} userInfo := &googleAPI.UserInfo{ID: googleID} - user := repo.FindOrCreateByGoogleInfo(userInfo) + user := suite.Repo.FindOrCreateByGoogleInfo(userInfo) assert.Equal(t, "10001", user.GoogleID) }) @@ -29,12 +45,11 @@ func TestDBUserRepository_FindOrCreateByGoogleInfo(t *testing.T) { t.Run("when user exists", func(t *testing.T) { googleID := "10002" - TestDB.Create(&User{GoogleID: googleID}) + suite.DB.Create(&User{GoogleID: googleID}) - repo := DBUserRepository{DB: TestDB} userInfo := &googleAPI.UserInfo{ID: googleID} - user := repo.FindOrCreateByGoogleInfo(userInfo) + user := suite.Repo.FindOrCreateByGoogleInfo(userInfo) assert.Equal(t, "10002", user.GoogleID) }) From 4dfc7981ce661cad60a54b2f865c8c7b636def7e Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Tue, 12 May 2020 21:43:33 +0200 Subject: [PATCH 19/25] Add tests for TimecodeRepository --- cmd/timecode_repository.go | 22 +++---- cmd/timecode_repository_test.go | 105 ++++++++++++++++++++++++++++++++ cmd/timecodes_controller.go | 27 +++----- 3 files changed, 126 insertions(+), 28 deletions(-) create mode 100644 cmd/timecode_repository_test.go diff --git a/cmd/timecode_repository.go b/cmd/timecode_repository.go index 09b3582..c25ddf6 100644 --- a/cmd/timecode_repository.go +++ b/cmd/timecode_repository.go @@ -17,25 +17,26 @@ type Timecode struct { } type TimecodeRepository interface { - FindByVideoId(string) (*[]*Timecode, error) + FindByVideoId(string) *[]*Timecode Create(*Timecode) (*Timecode, error) - CreateFromParsedCodes([]timecodeParser.ParsedTimeCode, string) (*[]*Timecode, error) + CreateFromParsedCodes([]timecodeParser.ParsedTimeCode, string) *[]*Timecode } type DBTimecodeRepository struct { + TimecodeRepository + DB *gorm.DB } -func (repo *DBTimecodeRepository) FindByVideoId(videoID string) (*[]*Timecode, error) { +func (repo *DBTimecodeRepository) FindByVideoId(videoID string) *[]*Timecode { timecodes := &[]*Timecode{} - err := repo.DB.Order("seconds asc"). + repo.DB.Order("seconds asc"). Preload("Likes"). Where(&Timecode{VideoID: videoID}). - Find(timecodes). - Error + Find(timecodes) - return timecodes, err + return timecodes } func (repo *DBTimecodeRepository) Create(timecode *Timecode) (*Timecode, error) { @@ -44,11 +45,10 @@ func (repo *DBTimecodeRepository) Create(timecode *Timecode) (*Timecode, error) return timecode, err } -func (repo *DBTimecodeRepository) CreateFromParsedCodes(parsedTimecodes []timecodeParser.ParsedTimeCode, videoId string) (*[]*Timecode, error) { +func (repo *DBTimecodeRepository) CreateFromParsedCodes(parsedTimecodes []timecodeParser.ParsedTimeCode, videoId string) *[]*Timecode { seen := make(map[string]struct{}) var collection []*Timecode - var err error for _, code := range parsedTimecodes { key := strconv.Itoa(code.Seconds) + code.Description if _, ok := seen[key]; ok { @@ -59,12 +59,12 @@ func (repo *DBTimecodeRepository) CreateFromParsedCodes(parsedTimecodes []timeco timecode := &Timecode{Seconds: code.Seconds, VideoID: videoId, Description: code.Description} - err = repo.DB.Create(timecode).Error + err := repo.DB.Create(timecode).Error if err != nil { continue } collection = append(collection, timecode) } - return &collection, err + return &collection } diff --git a/cmd/timecode_repository_test.go b/cmd/timecode_repository_test.go new file mode 100644 index 0000000..dd8d2eb --- /dev/null +++ b/cmd/timecode_repository_test.go @@ -0,0 +1,105 @@ +package main + +import ( + "testing" + timecodeParser "timecodes/cmd/timecode_parser" + + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +const videoID = "armenian-dram" +const anotherVideoID = "strategist" + +type TimecodeRepositorySuite struct { + suite.Suite + DB *gorm.DB + Repo *DBTimecodeRepository +} + +func (suite *TimecodeRepositorySuite) ResetDB() { + suite.DB.Exec("TRUNCATE TABLE timecodes;") +} + +func (suite *TimecodeRepositorySuite) SetupTest() { + db := setupTestDB() + suite.DB = db + suite.Repo = &DBTimecodeRepository{DB: db} +} + +func TestTimecodeRepositorySuite(t *testing.T) { + suite.Run(t, new(TimecodeRepositorySuite)) +} + +func (suite *TimecodeRepositorySuite) TestDBTimecodeRepository_FindByVideoId() { + t := suite.T() + + t.Run("when matching records exist", func(t *testing.T) { + suite.DB.Create(&Timecode{VideoID: videoID, Seconds: 55, Description: "ABC"}) + suite.DB.Create(&Timecode{VideoID: videoID, Seconds: 23, Description: "DEFG"}) + suite.DB.Create(&Timecode{VideoID: anotherVideoID, Seconds: 77, Description: "FGHJ"}) + defer suite.ResetDB() + + timecodes := *suite.Repo.FindByVideoId(videoID) + + assert.Equal(t, 2, len(timecodes)) + assert.Equal(t, 23, timecodes[0].Seconds) + assert.Equal(t, 55, timecodes[1].Seconds) + }) + + t.Run("when there are no matching records", func(t *testing.T) { + suite.DB.Create(&Timecode{VideoID: anotherVideoID, Seconds: 77, Description: "FGHJ"}) + defer suite.ResetDB() + + timecodes := *suite.Repo.FindByVideoId(videoID) + + assert.Equal(t, 0, len(timecodes)) + }) +} + +func (suite *TimecodeRepositorySuite) TestDBTimecodeRepository_Create() { + t := suite.T() + + t.Run("when record has been created", func(t *testing.T) { + defer suite.ResetDB() + + timecode, err := suite.Repo.Create(&Timecode{VideoID: videoID, Seconds: 55, Description: "ABC"}) + + assert.Nil(t, err) + assert.NotNil(t, timecode.ID) + assert.Equal(t, videoID, timecode.VideoID) + }) + + t.Run("when db returns an error", func(t *testing.T) { + seconds := 10 + description := "ABC" + + suite.DB.Create(&Timecode{VideoID: videoID, Seconds: seconds, Description: description}) + defer suite.ResetDB() + + timecode, err := suite.Repo.Create(&Timecode{VideoID: videoID, Seconds: seconds, Description: description}) + + assert.True(t, suite.DB.NewRecord(timecode)) + assert.EqualError(t, err, `pq: duplicate key value violates unique constraint "idx_timecodes_seconds_text_video_id"`) + }) +} + +func (suite *TimecodeRepositorySuite) TestDBTimecodeRepository_CreateFromParsedCodes() { + suite.T().Run("when valid parsed codes has been given", func(t *testing.T) { + parsedTimecodes := []timecodeParser.ParsedTimeCode{ + {Seconds: 24, Description: "ABC"}, + {Seconds: 24, Description: "ABC"}, + {Seconds: 56, Description: "DFG"}, + {Seconds: 56, Description: "DFG"}, + {Seconds: 56, Description: "DFG"}, + } + defer suite.ResetDB() + + timecodes := *suite.Repo.CreateFromParsedCodes(parsedTimecodes, videoID) + + assert.Equal(t, 2, len(timecodes)) + assert.Equal(t, 24, timecodes[0].Seconds) + assert.Equal(t, 56, timecodes[1].Seconds) + }) +} diff --git a/cmd/timecodes_controller.go b/cmd/timecodes_controller.go index 7b85c9c..23cb95b 100644 --- a/cmd/timecodes_controller.go +++ b/cmd/timecodes_controller.go @@ -3,7 +3,6 @@ package main import ( "encoding/json" "io/ioutil" - "log" "net/http" "github.com/gorilla/mux" @@ -25,21 +24,18 @@ func handleGetTimecodes(c *Container, w http.ResponseWriter, r *http.Request) { currentUser := getCurrentUser(r) videoID := mux.Vars(r)["videoId"] - timecodes, err := c.TimecodeRepository.FindByVideoId(videoID) - if err != nil { - json.NewEncoder(w).Encode(err) - } else { - if len(*timecodes) == 0 { - go parseVideoContentAndCreateTimecodes(c, videoID) - } + timecodes := c.TimecodeRepository.FindByVideoId(videoID) - timecodeJSONCollection := make([]*TimecodeJSON, 0) - for _, timecode := range *timecodes { - timecodeJSONCollection = append(timecodeJSONCollection, serializeTimecode(timecode, currentUser)) - } + if len(*timecodes) == 0 { + go parseVideoContentAndCreateTimecodes(c, videoID) + } - json.NewEncoder(w).Encode(timecodeJSONCollection) + timecodeJSONCollection := make([]*TimecodeJSON, 0) + for _, timecode := range *timecodes { + timecodeJSONCollection = append(timecodeJSONCollection, serializeTimecode(timecode, currentUser)) } + + json.NewEncoder(w).Encode(timecodeJSONCollection) } // POST /timecodes @@ -99,8 +95,5 @@ func parseVideoContentAndCreateTimecodes(c *Container, videoID string) { parsedCodes = append(parsedCodes, timeCodes...) } - _, err := c.TimecodeRepository.CreateFromParsedCodes(parsedCodes, videoID) - if err != nil { - log.Println(err) - } + c.TimecodeRepository.CreateFromParsedCodes(parsedCodes, videoID) } From 795dabfa6ab357c680dd9054d7aa0aca6b1d3aa1 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Sat, 16 May 2020 12:30:04 +0200 Subject: [PATCH 20/25] Fix repository tests --- .air.conf | 2 +- .env.example | 4 +- .env.test | 4 +- cmd/db.go | 65 +++----- cmd/db_migrations.go | 35 +++++ cmd/main_test.go | 23 +++ cmd/timecode_like_repository.go | 6 +- cmd/timecode_like_repository_test.go | 102 +++++++++++++ cmd/timecode_repository.go | 2 +- cmd/timecode_repository_test.go | 22 ++- cmd/user_repository_test.go | 13 +- coverage.out | 129 ++++++++++++++++ docker-compose.yml | 3 +- go.mod | 6 +- go.sum | 217 ++++++++++++++++++++++++++- 15 files changed, 560 insertions(+), 73 deletions(-) create mode 100644 cmd/db_migrations.go create mode 100644 cmd/main_test.go create mode 100644 cmd/timecode_like_repository_test.go create mode 100644 coverage.out diff --git a/.air.conf b/.air.conf index b54727e..db0fd26 100644 --- a/.air.conf +++ b/.air.conf @@ -11,7 +11,7 @@ cmd = "go build -o ./tmp/main ./cmd" # Binary file yields from `cmd`. bin = "tmp/main" # Customize binary. -full_bin = "APP_ENV=dev APP_USER=air ./tmp/main" +full_bin = "APP_ENV=development APP_USER=air ./tmp/main" # Watch these filename extensions. include_ext = ["go"] # Ignore these filename extNensions or directories. diff --git a/.env.example b/.env.example index 619873c..ee205ae 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,10 @@ +APP_ENV=development PORT=8080 PG_USER=postgres PG_PASSWORD=postgres PG_HOST=db PG_PORT=5432 -PG_DB=timecodes_db + +POSTGRES_DB=timecodes_development GOOGLE_API_KEY= diff --git a/.env.test b/.env.test index ee3f804..bbdcd55 100644 --- a/.env.test +++ b/.env.test @@ -1,8 +1,10 @@ +APP_ENV=test PORT=8080 PG_USER=postgres PG_PASSWORD=postgres PG_HOST=db PG_PORT=5432 -PG_DB=timecodes_db_test + +POSTGRES_DB=timecodes_test GOOGLE_API_KEY= diff --git a/cmd/db.go b/cmd/db.go index c1506bd..7b7ccaf 100644 --- a/cmd/db.go +++ b/cmd/db.go @@ -5,44 +5,54 @@ import ( "log" "net/url" "os" - "sync" - "github.com/DATA-DOG/go-txdb" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/postgres" ) -const TXDB_NAME = "txdb_postgres" -const TXDB_CONNECTION_POOL_NAME = "timecodes_connection_pool" +func getDbName() string { + return fmt.Sprintf("timecodes_%s", os.Getenv("APP_ENV")) +} -var doOnce sync.Once +func getEnvDSN() url.URL { + return getDsn(getDbName()) +} func initDB() *gorm.DB { - envDsn := getDsn(os.Getenv("PG_DB")) + envDsn := getEnvDSN() db, err := gorm.Open("postgres", envDsn.String()) if err != nil { - db = createDatabase(os.Getenv("PG_DB")) + dbName := getDbName() + db = createDatabase(dbName) } + log.Println("Connection to the database has been established.") + return db } func createDatabase(dbName string) *gorm.DB { - defaultDsn := getDsn("postgres") - db, err := gorm.Open("postgres", defaultDsn.String()) + db := getDefaultConnection() + + err := db.Exec(fmt.Sprintf("CREATE DATABASE %s;", dbName)).Error if err != nil { handleDBConnectionError(err) } - err = db.Exec(fmt.Sprintf("CREATE DATABASE %s;", dbName)).Error + dsn := getDsn(dbName) + + db, err = gorm.Open("postgres", dsn.String()) if err != nil { handleDBConnectionError(err) } - dsn := getDsn(dbName) + return db +} - db, err = gorm.Open("postgres", dsn.String()) +func getDefaultConnection() *gorm.DB { + defaultDsn := getDsn("postgres") + db, err := gorm.Open("postgres", defaultDsn.String()) if err != nil { handleDBConnectionError(err) } @@ -64,34 +74,3 @@ func getDsn(path string) url.URL { RawQuery: (&url.Values{"sslmode": []string{"disable"}}).Encode(), } } - -func runMigrations(db *gorm.DB) { - db.AutoMigrate(&Timecode{}) - db.Model(&Timecode{}).AddUniqueIndex( - "idx_timecodes_seconds_text_video_id", - "seconds", "description", "video_id", - ) - db.AutoMigrate(&User{}) - db.AutoMigrate(&TimecodeLike{}) - db.Model(&TimecodeLike{}).AddUniqueIndex( - "idx_timecodes_likes_user_id_timecode_id_video_id", - "user_id", "timecode_id", - ) -} - -func setupTestDB() *gorm.DB { - initDB() - dsn := getDsn(os.Getenv("PG_DB")) - doOnce.Do(func() { - txdb.Register(TXDB_NAME, "postgres", dsn.String()) - }) - - db, err := gorm.Open("postgres", TXDB_NAME, TXDB_CONNECTION_POOL_NAME) - if err != nil { - handleDBConnectionError(err) - } - - runMigrations(db) - - return db -} diff --git a/cmd/db_migrations.go b/cmd/db_migrations.go new file mode 100644 index 0000000..ce7fbec --- /dev/null +++ b/cmd/db_migrations.go @@ -0,0 +1,35 @@ +package main + +import "github.com/jinzhu/gorm" + +func runMigrations(db *gorm.DB) { + applyTimecodesMigrations(db) + applyUsersMigrations(db) + applyTimecodeLikesMigrations(db) +} + +func applyTimecodesMigrations(db *gorm.DB) { + db.AutoMigrate(&Timecode{}) + db.Model(&Timecode{}).AddUniqueIndex( + "idx_timecodes_seconds_text_video_id", + "seconds", "description", "video_id", + ) + db.Exec(` + ALTER TABLE timecodes + ADD CONSTRAINT description_min_length CHECK (length(description) >= 1); + `) +} + +func applyUsersMigrations(db *gorm.DB) { + db.AutoMigrate(&User{}) +} + +func applyTimecodeLikesMigrations(db *gorm.DB) { + db.AutoMigrate(&TimecodeLike{}) + db.Model(&TimecodeLike{}).AddUniqueIndex( + "idx_timecodes_likes_user_id_timecode_id_video_id", + "user_id", "timecode_id", + ) + db.Model(&TimecodeLike{}).AddForeignKey("timecode_id", "timecodes(id)", "RESTRICT", "RESTRICT") + db.Model(&TimecodeLike{}).AddForeignKey("user_id", "users(id)", "RESTRICT", "RESTRICT") +} diff --git a/cmd/main_test.go b/cmd/main_test.go new file mode 100644 index 0000000..806d945 --- /dev/null +++ b/cmd/main_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "os" + "testing" + + "github.com/khaiql/dbcleaner/engine" + "gopkg.in/khaiql/dbcleaner.v2" +) + +var Cleaner = dbcleaner.New() +var TestDB = initDB() + +func TestMain(m *testing.M) { + dsn := getEnvDSN() + pg := engine.NewPostgresEngine(dsn.String()) + Cleaner.SetEngine(pg) + + runMigrations(TestDB) + defer TestDB.Close() + + os.Exit(m.Run()) +} diff --git a/cmd/timecode_like_repository.go b/cmd/timecode_like_repository.go index 2725b03..6f7878f 100644 --- a/cmd/timecode_like_repository.go +++ b/cmd/timecode_like_repository.go @@ -14,6 +14,8 @@ type TimecodeLikeRepository interface { } type DBTimecodeLikeRepository struct { + TimecodeLikeRepository + DB *gorm.DB } @@ -31,7 +33,7 @@ func (repo *DBTimecodeLikeRepository) Delete(timecodeLike *TimecodeLike, userID return nil, err } - err = repo.DB.Unscoped().Delete(timecodeLike).Error + repo.DB.Unscoped().Delete(timecodeLike) - return timecodeLike, err + return timecodeLike, nil } diff --git a/cmd/timecode_like_repository_test.go b/cmd/timecode_like_repository_test.go new file mode 100644 index 0000000..aa7a23f --- /dev/null +++ b/cmd/timecode_like_repository_test.go @@ -0,0 +1,102 @@ +package main + +import ( + "testing" + + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type TimecodeLikeRepositorySuite struct { + suite.Suite + DB *gorm.DB + Repo *DBTimecodeLikeRepository +} + +func (suite *TimecodeLikeRepositorySuite) SetupSuite() { + suite.DB = TestDB + suite.Repo = &DBTimecodeLikeRepository{DB: TestDB} +} + +func (suite *TimecodeLikeRepositorySuite) SetupTest() { + for _, table := range []string{ + "timecode_likes", + "timecodes", + "users", + } { + Cleaner.Acquire(table) + } +} + +func (suite *TimecodeLikeRepositorySuite) TearDownTest() { + for _, table := range []string{ + "timecode_likes", + "timecodes", + "users", + } { + Cleaner.Clean(table) + } +} + +func TestTimecodeLikeRepositorySuite(t *testing.T) { + suite.Run(t, new(TimecodeLikeRepositorySuite)) +} + +func (suite *TimecodeLikeRepositorySuite) TestDBTimecodeLikeRepository_Create() { + st := suite.T() + + st.Run("when valid parameters given", func(t *testing.T) { + user := &User{Email: "user1@example.com"} + timecode := &Timecode{VideoID: "video-id-1", Description: "test"} + suite.DB.Create(user) + suite.DB.Create(timecode) + + timecodeLike := &TimecodeLike{TimecodeID: timecode.ID} + timecodeLike, err := suite.Repo.Create(timecodeLike, user.ID) + + assert.Equal(t, user.ID, timecodeLike.UserID) + assert.Equal(t, timecode.ID, timecodeLike.TimecodeID) + assert.Nil(t, err) + }) + + st.Run("when invalid parameters given", func(t *testing.T) { + user := &User{Email: "user2@example.com"} + suite.DB.Create(user) + + timecodeLike := &TimecodeLike{TimecodeID: uint(1984)} + timecodeLike, err := suite.Repo.Create(timecodeLike, user.ID) + + assert.True(t, suite.DB.NewRecord(timecodeLike)) + assert.EqualError( + t, err, + `pq: insert or update on table "timecode_likes" violates foreign key constraint "timecode_likes_timecode_id_timecodes_id_foreign"`) + }) +} + +func (suite *TimecodeLikeRepositorySuite) TestDBTimecodeLikeRepository_Delete() { + st := suite.T() + + st.Run("when record exists", func(t *testing.T) { + user := &User{Email: "user3@example.com"} + timecode := &Timecode{VideoID: "video-id-2", Description: "test"} + suite.DB.Create(user) + suite.DB.Create(timecode) + timecodeLike := &TimecodeLike{TimecodeID: timecode.ID, UserID: user.ID} + suite.DB.Create(timecodeLike) + + timecodeLike, err := suite.Repo.Delete(timecodeLike, user.ID) + + assert.True(t, suite.DB.First(timecodeLike).RecordNotFound()) + assert.Nil(t, err) + }) + + st.Run("when record doesn't exist", func(t *testing.T) { + timecodeLike := &TimecodeLike{TimecodeID: 4, UserID: 5} + + timecodeLike, err := suite.Repo.Delete(timecodeLike, 6) + + assert.Nil(t, timecodeLike) + assert.EqualError(t, err, "record not found") + }) +} diff --git a/cmd/timecode_repository.go b/cmd/timecode_repository.go index c25ddf6..4aa54f3 100644 --- a/cmd/timecode_repository.go +++ b/cmd/timecode_repository.go @@ -10,7 +10,7 @@ import ( // Timecode represents timecode model type Timecode struct { gorm.Model - Description string `json:"description"` + Description string `json:"description" gorm:"not null;default:null"` Seconds int `json:"seconds" gorm:"not null"` VideoID string `json:"videoId" gorm:"not null;index"` Likes []TimecodeLike `json:"likes" gorm:"foreignkey:TimecodeID"` diff --git a/cmd/timecode_repository_test.go b/cmd/timecode_repository_test.go index dd8d2eb..30acf20 100644 --- a/cmd/timecode_repository_test.go +++ b/cmd/timecode_repository_test.go @@ -18,14 +18,17 @@ type TimecodeRepositorySuite struct { Repo *DBTimecodeRepository } -func (suite *TimecodeRepositorySuite) ResetDB() { - suite.DB.Exec("TRUNCATE TABLE timecodes;") +func (suite *TimecodeRepositorySuite) SetupSuite() { + suite.DB = TestDB + suite.Repo = &DBTimecodeRepository{DB: TestDB} } func (suite *TimecodeRepositorySuite) SetupTest() { - db := setupTestDB() - suite.DB = db - suite.Repo = &DBTimecodeRepository{DB: db} + Cleaner.Acquire("timecodes") +} + +func (suite *TimecodeRepositorySuite) TearDownTest() { + Cleaner.Clean("timecodes") } func TestTimecodeRepositorySuite(t *testing.T) { @@ -39,7 +42,7 @@ func (suite *TimecodeRepositorySuite) TestDBTimecodeRepository_FindByVideoId() { suite.DB.Create(&Timecode{VideoID: videoID, Seconds: 55, Description: "ABC"}) suite.DB.Create(&Timecode{VideoID: videoID, Seconds: 23, Description: "DEFG"}) suite.DB.Create(&Timecode{VideoID: anotherVideoID, Seconds: 77, Description: "FGHJ"}) - defer suite.ResetDB() + defer Cleaner.Clean("timecodes") timecodes := *suite.Repo.FindByVideoId(videoID) @@ -50,7 +53,6 @@ func (suite *TimecodeRepositorySuite) TestDBTimecodeRepository_FindByVideoId() { t.Run("when there are no matching records", func(t *testing.T) { suite.DB.Create(&Timecode{VideoID: anotherVideoID, Seconds: 77, Description: "FGHJ"}) - defer suite.ResetDB() timecodes := *suite.Repo.FindByVideoId(videoID) @@ -62,8 +64,6 @@ func (suite *TimecodeRepositorySuite) TestDBTimecodeRepository_Create() { t := suite.T() t.Run("when record has been created", func(t *testing.T) { - defer suite.ResetDB() - timecode, err := suite.Repo.Create(&Timecode{VideoID: videoID, Seconds: 55, Description: "ABC"}) assert.Nil(t, err) @@ -76,7 +76,6 @@ func (suite *TimecodeRepositorySuite) TestDBTimecodeRepository_Create() { description := "ABC" suite.DB.Create(&Timecode{VideoID: videoID, Seconds: seconds, Description: description}) - defer suite.ResetDB() timecode, err := suite.Repo.Create(&Timecode{VideoID: videoID, Seconds: seconds, Description: description}) @@ -92,9 +91,8 @@ func (suite *TimecodeRepositorySuite) TestDBTimecodeRepository_CreateFromParsedC {Seconds: 24, Description: "ABC"}, {Seconds: 56, Description: "DFG"}, {Seconds: 56, Description: "DFG"}, - {Seconds: 56, Description: "DFG"}, + {Seconds: 56, Description: ""}, } - defer suite.ResetDB() timecodes := *suite.Repo.CreateFromParsedCodes(parsedTimecodes, videoID) diff --git a/cmd/user_repository_test.go b/cmd/user_repository_test.go index 2d0509c..6988f22 100644 --- a/cmd/user_repository_test.go +++ b/cmd/user_repository_test.go @@ -16,14 +16,17 @@ type UserRepositorySuite struct { Repo *DBUserRepository } -func (suite *UserRepositorySuite) ResetDB() { - suite.DB.Exec("TRUNCATE TABLE users;") +func (suite *UserRepositorySuite) SetupSuite() { + suite.DB = TestDB + suite.Repo = &DBUserRepository{DB: TestDB} } func (suite *UserRepositorySuite) SetupTest() { - db := setupTestDB() - suite.DB = db - suite.Repo = &DBUserRepository{DB: db} + Cleaner.Acquire("users") +} + +func (suite *UserRepositorySuite) TearDownTest() { + Cleaner.Clean("users") } func TestUserRepositorySuite(t *testing.T) { diff --git a/coverage.out b/coverage.out new file mode 100644 index 0000000..7f7f996 --- /dev/null +++ b/coverage.out @@ -0,0 +1,129 @@ +mode: set +timecodes/cmd/timecode_parser/timecode_parser.go:27.58,35.2 5 1 +timecodes/cmd/timecode_parser/timecode_parser.go:37.57,40.82 2 1 +timecodes/cmd/timecode_parser/timecode_parser.go:46.2,46.13 1 1 +timecodes/cmd/timecode_parser/timecode_parser.go:40.82,41.38 1 1 +timecodes/cmd/timecode_parser/timecode_parser.go:41.38,43.4 1 1 +timecodes/cmd/timecode_parser/timecode_parser.go:49.72,50.37 1 1 +timecodes/cmd/timecode_parser/timecode_parser.go:54.2,54.34 1 1 +timecodes/cmd/timecode_parser/timecode_parser.go:67.2,67.19 1 1 +timecodes/cmd/timecode_parser/timecode_parser.go:50.37,52.3 1 1 +timecodes/cmd/timecode_parser/timecode_parser.go:54.34,65.3 6 1 +timecodes/cmd/timecode_parser/timecode_parser.go:70.46,73.35 2 1 +timecodes/cmd/timecode_parser/timecode_parser.go:79.2,83.20 4 1 +timecodes/cmd/timecode_parser/timecode_parser.go:73.35,75.3 1 1 +timecodes/cmd/timecode_parser/timecode_parser.go:75.8,77.3 1 1 +timecodes/cmd/timecode_parser/timecode_parser.go:86.46,90.36 3 1 +timecodes/cmd/timecode_parser/timecode_parser.go:96.2,96.16 1 1 +timecodes/cmd/timecode_parser/timecode_parser.go:90.36,94.3 3 1 +timecodes/cmd/google_api/google_api.go:25.35,27.2 1 1 +timecodes/cmd/google_api/google_api.go:29.72,30.27 1 1 +timecodes/cmd/google_api/google_api.go:34.2,36.16 3 1 +timecodes/cmd/google_api/google_api.go:40.2,42.16 3 1 +timecodes/cmd/google_api/google_api.go:46.2,46.29 1 1 +timecodes/cmd/google_api/google_api.go:64.2,64.22 1 1 +timecodes/cmd/google_api/google_api.go:30.27,32.3 1 1 +timecodes/cmd/google_api/google_api.go:36.16,38.3 1 1 +timecodes/cmd/google_api/google_api.go:42.16,44.3 1 1 +timecodes/cmd/google_api/google_api.go:47.11,50.26 3 1 +timecodes/cmd/google_api/google_api.go:54.3,54.17 1 1 +timecodes/cmd/google_api/google_api.go:55.10,59.17 4 1 +timecodes/cmd/google_api/google_api.go:50.26,52.4 1 0 +timecodes/cmd/google_api/google_api.go:59.17,61.4 1 0 +timecodes/cmd/timecode_likes_controller.go:11.85,17.16 5 0 +timecodes/cmd/timecode_likes_controller.go:22.2,24.16 2 0 +timecodes/cmd/timecode_likes_controller.go:17.16,20.3 2 0 +timecodes/cmd/timecode_likes_controller.go:24.16,26.3 1 0 +timecodes/cmd/timecode_likes_controller.go:26.8,28.3 1 0 +timecodes/cmd/timecode_likes_controller.go:32.85,38.16 5 0 +timecodes/cmd/timecode_likes_controller.go:43.2,44.16 2 0 +timecodes/cmd/timecode_likes_controller.go:38.16,41.3 2 0 +timecodes/cmd/timecode_likes_controller.go:44.16,46.3 1 0 +timecodes/cmd/timecode_likes_controller.go:46.8,48.3 1 0 +timecodes/cmd/db.go:13.25,15.2 1 1 +timecodes/cmd/db.go:17.26,19.2 1 1 +timecodes/cmd/db.go:21.24,25.16 3 1 +timecodes/cmd/db.go:30.2,32.11 2 1 +timecodes/cmd/db.go:25.16,28.3 2 0 +timecodes/cmd/db.go:35.45,39.16 3 0 +timecodes/cmd/db.go:43.2,46.16 3 0 +timecodes/cmd/db.go:50.2,50.11 1 0 +timecodes/cmd/db.go:39.16,41.3 1 0 +timecodes/cmd/db.go:46.16,48.3 1 0 +timecodes/cmd/db.go:53.38,56.16 3 0 +timecodes/cmd/db.go:60.2,60.11 1 0 +timecodes/cmd/db.go:56.16,58.3 1 0 +timecodes/cmd/db.go:63.41,65.12 2 0 +timecodes/cmd/db.go:68.34,76.2 1 1 +timecodes/cmd/timecode_like_repository.go:22.110,28.2 3 1 +timecodes/cmd/timecode_like_repository.go:30.110,32.16 2 1 +timecodes/cmd/timecode_like_repository.go:36.2,38.26 2 1 +timecodes/cmd/timecode_like_repository.go:32.16,34.3 1 1 +timecodes/cmd/db_migrations.go:5.33,9.2 3 1 +timecodes/cmd/db_migrations.go:11.44,21.2 3 1 +timecodes/cmd/db_migrations.go:23.40,25.2 1 1 +timecodes/cmd/db_migrations.go:27.48,35.2 4 1 +timecodes/cmd/main.go:9.13,14.16 4 0 +timecodes/cmd/main.go:18.2,25.28 2 0 +timecodes/cmd/main.go:14.16,16.3 1 0 +timecodes/cmd/router.go:18.68,20.2 1 0 +timecodes/cmd/router.go:22.44,47.2 14 0 +timecodes/cmd/router.go:49.55,50.71 1 0 +timecodes/cmd/router.go:50.71,53.3 2 0 +timecodes/cmd/router.go:56.57,58.2 1 0 +timecodes/cmd/router.go:60.44,62.17 2 0 +timecodes/cmd/router.go:66.2,66.12 1 0 +timecodes/cmd/router.go:62.17,64.3 1 0 +timecodes/cmd/timecode_repository.go:31.78,40.2 3 1 +timecodes/cmd/timecode_repository.go:42.81,46.2 2 1 +timecodes/cmd/timecode_repository.go:48.135,52.39 3 1 +timecodes/cmd/timecode_repository.go:69.2,69.20 1 1 +timecodes/cmd/timecode_repository.go:52.39,54.29 2 1 +timecodes/cmd/timecode_repository.go:58.3,63.17 4 1 +timecodes/cmd/timecode_repository.go:66.3,66.44 1 1 +timecodes/cmd/timecode_repository.go:54.29,55.12 1 1 +timecodes/cmd/timecode_repository.go:63.17,64.12 1 1 +timecodes/cmd/timecodes_controller.go:23.79,29.26 4 0 +timecodes/cmd/timecodes_controller.go:33.2,34.38 2 0 +timecodes/cmd/timecodes_controller.go:38.2,38.51 1 0 +timecodes/cmd/timecodes_controller.go:29.26,31.3 1 0 +timecodes/cmd/timecodes_controller.go:34.38,36.3 1 0 +timecodes/cmd/timecodes_controller.go:42.81,48.16 5 0 +timecodes/cmd/timecodes_controller.go:52.2,53.16 2 0 +timecodes/cmd/timecodes_controller.go:48.16,51.3 2 0 +timecodes/cmd/timecodes_controller.go:53.16,55.3 1 0 +timecodes/cmd/timecodes_controller.go:55.8,57.3 1 0 +timecodes/cmd/timecodes_controller.go:60.92,62.24 2 0 +timecodes/cmd/timecodes_controller.go:66.2,73.3 1 0 +timecodes/cmd/timecodes_controller.go:62.24,64.3 1 0 +timecodes/cmd/timecodes_controller.go:76.59,77.29 1 0 +timecodes/cmd/timecodes_controller.go:83.2,83.14 1 0 +timecodes/cmd/timecodes_controller.go:77.29,78.28 1 0 +timecodes/cmd/timecodes_controller.go:78.28,80.4 1 0 +timecodes/cmd/timecodes_controller.go:86.72,92.35 4 0 +timecodes/cmd/timecodes_controller.go:98.2,98.66 1 0 +timecodes/cmd/timecodes_controller.go:92.35,96.3 2 0 +timecodes/cmd/auth_controller.go:8.58,10.2 1 0 +timecodes/cmd/auth_middleware.go:15.72,16.44 1 0 +timecodes/cmd/auth_middleware.go:33.2,33.8 1 0 +timecodes/cmd/auth_middleware.go:16.44,17.72 1 0 +timecodes/cmd/auth_middleware.go:17.72,21.18 3 0 +timecodes/cmd/auth_middleware.go:26.4,30.24 4 0 +timecodes/cmd/auth_middleware.go:21.18,24.5 2 0 +timecodes/cmd/auth_middleware.go:36.54,39.23 2 0 +timecodes/cmd/auth_middleware.go:43.2,45.22 2 0 +timecodes/cmd/auth_middleware.go:39.23,41.3 1 0 +timecodes/cmd/user_repository.go:26.92,34.2 3 1 +timecodes/cmd/youtube_api/youtube_api.go:18.30,20.16 2 1 +timecodes/cmd/youtube_api/youtube_api.go:24.2,24.46 1 1 +timecodes/cmd/youtube_api/youtube_api.go:20.16,22.3 1 1 +timecodes/cmd/youtube_api/youtube_api.go:27.64,34.16 3 1 +timecodes/cmd/youtube_api/youtube_api.go:40.2,41.21 2 1 +timecodes/cmd/youtube_api/youtube_api.go:45.2,45.37 1 1 +timecodes/cmd/youtube_api/youtube_api.go:34.16,38.3 2 1 +timecodes/cmd/youtube_api/youtube_api.go:41.21,43.3 1 1 +timecodes/cmd/youtube_api/youtube_api.go:48.63,59.16 4 1 +timecodes/cmd/youtube_api/youtube_api.go:65.2,65.38 1 1 +timecodes/cmd/youtube_api/youtube_api.go:69.2,69.21 1 1 +timecodes/cmd/youtube_api/youtube_api.go:59.16,63.3 2 1 +timecodes/cmd/youtube_api/youtube_api.go:65.38,67.3 1 1 diff --git a/docker-compose.yml b/docker-compose.yml index 74f298d..cd6afd3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,7 +47,8 @@ services: environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - - POSTGRES_DB=timecodes_db + env_file: + - .env ports: - 9000:5432 nginx: diff --git a/go.mod b/go.mod index 3b3768a..3ab13b4 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,13 @@ module timecodes go 1.13 require ( - github.com/DATA-DOG/go-txdb v0.1.3 + github.com/alexflint/go-filemutex v1.1.0 // indirect github.com/gorilla/mux v1.7.4 github.com/jarcoal/httpmock v1.0.5 github.com/jinzhu/gorm v1.9.12 + github.com/khaiql/dbcleaner v2.3.0+incompatible github.com/rs/cors v1.7.0 github.com/stretchr/testify v1.5.1 - google.golang.org/api v0.21.0 + google.golang.org/api v0.24.0 + gopkg.in/khaiql/dbcleaner.v2 v2.3.0 ) diff --git a/go.sum b/go.sum index c244889..d325eff 100644 --- a/go.sum +++ b/go.sum @@ -2,36 +2,88 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.56.0 h1:WRz29PgAsVEyPSDHyk+0fpEkwEFyfhHn+JbksT6gIL4= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/DATA-DOG/go-txdb v0.1.3 h1:R4v6OuOcy2O147e2zHxU0B4NDtF+INb5R9q/CV7AEMg= -github.com/DATA-DOG/go-txdb v0.1.3/go.mod h1:DhAhxMXZpUJVGnT+p9IbzJoRKvlArO2pkHjnGX7o0n0= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/alexflint/go-filemutex v1.1.0 h1:IAWuUuRYL2hETx5b8vCgwnD+xSdlsTQY6s2JjBsqLdg= +github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -40,6 +92,7 @@ github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jarcoal/httpmock v1.0.5 h1:cHtVEcTxRSX4J0je7mWPfc9BpDpqzXSJ5HbymZmyHck= github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= @@ -49,6 +102,15 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/khaiql/dbcleaner v2.3.0+incompatible h1:VU/ZnMcs0Dx6s4XELYfZMMyib2hyrnJ0Xk1/2aZQIqg= +github.com/khaiql/dbcleaner v2.3.0+incompatible/go.mod h1:NUURNSEp3cHXCm37Ljb/IWAdp2/qYv/HAW+1BdnEbps= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw= @@ -56,76 +118,223 @@ github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4 h1:kDtqNkeBrZb8B+atrj50B5XLHpzXXqcCdZPP/ApQ5NY= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.21.0 h1:zS+Q/CJJnVlXpXQVIz+lH0ZT2lBuT2ac7XD8Y/3w6hY= -google.golang.org/api v0.21.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0 h1:cG03eaksBzhfSIk7JRGctfp3lanklcOM/mTGvow7BbQ= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940 h1:MRHtG0U6SnaUb+s+LhNE1qt1FQ1wlhqr5E4usBKC0uA= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +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/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/khaiql/dbcleaner.v2 v2.3.0 h1:9tRNEo5tn7MhpHySz5Rstjt7iuTl2oIMRLYlLUf1BOA= +gopkg.in/khaiql/dbcleaner.v2 v2.3.0/go.mod h1:kzKBqwVHZ7o00FtbCfDKRDNfSkPGAgDJMQ8bd6ruy30= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From c2195907a3bfcc54b43451663c9027316ff2a0d0 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Tue, 19 May 2020 17:12:56 +0200 Subject: [PATCH 21/25] Add tests for some router methods --- cmd/auth_middleware.go | 6 ++ cmd/main.go | 10 +- cmd/main_test.go | 16 +++ cmd/router.go | 10 +- cmd/timecode_likes_controller.go | 8 ++ cmd/timecode_likes_controller_test.go | 150 ++++++++++++++++++++++++++ cmd/timecodes_controller.go | 8 +- cmd/timecodes_controller_test.go | 106 ++++++++++++++++++ coverage.out | 129 ---------------------- 9 files changed, 302 insertions(+), 141 deletions(-) create mode 100644 cmd/timecode_likes_controller_test.go create mode 100644 cmd/timecodes_controller_test.go delete mode 100644 coverage.out diff --git a/cmd/auth_middleware.go b/cmd/auth_middleware.go index bda029e..19538fe 100644 --- a/cmd/auth_middleware.go +++ b/cmd/auth_middleware.go @@ -15,6 +15,12 @@ type CurrentUserKey struct{} func authMiddleware(c *Container) (mw func(http.Handler) http.Handler) { mw = func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + currentUser := getCurrentUser(r) + if currentUser != nil { + next.ServeHTTP(w, r) + return + } + token := getAuthToken(r.Header.Get("Authorization")) userInfo, err := googleAPI.FetchUserInfo(token) diff --git a/cmd/main.go b/cmd/main.go index 8b364f6..59450a6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,6 +2,7 @@ package main import ( "log" + "net/http" youtubeAPI "timecodes/cmd/youtube_api" ) @@ -10,7 +11,7 @@ func main() { db := initDB() runMigrations(db) - youtubeAPI, err := youtubeAPI.New() + ytService, err := youtubeAPI.New() if err != nil { log.Fatal(err) } @@ -19,8 +20,11 @@ func main() { UserRepository: &DBUserRepository{DB: db}, TimecodeRepository: &DBTimecodeRepository{DB: db}, TimecodeLikeRepository: &DBTimecodeLikeRepository{DB: db}, - YoutubeAPI: youtubeAPI, + YoutubeAPI: ytService, } - startHttpServer(container) + router := createRouter(container) + + log.Println("Starting development server at http://127.0.0.1:8080/") + log.Fatal(http.ListenAndServe(":8080", router)) } diff --git a/cmd/main_test.go b/cmd/main_test.go index 806d945..299b75b 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -1,6 +1,9 @@ package main import ( + "context" + "net/http" + "net/http/httptest" "os" "testing" @@ -21,3 +24,16 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } + +func executeRequest(t *testing.T, router http.Handler, req *http.Request, user *User) *httptest.ResponseRecorder { + t.Helper() + + req.Header.Set("Content-Type", "application/json") + ctx := context.WithValue(context.Background(), CurrentUserKey{}, user) + req = req.WithContext(ctx) + + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + return rr +} diff --git a/cmd/router.go b/cmd/router.go index 13a1914..56e61ae 100644 --- a/cmd/router.go +++ b/cmd/router.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "log" "net/http" "github.com/gorilla/mux" @@ -19,9 +18,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.H(h.Container, w, r) } -func startHttpServer(container *Container) { - log.Println("Starting development server at http://127.0.0.1:8080/") - +func createRouter(container *Container) http.Handler { router := mux.NewRouter().StrictSlash(true) router.Use(commonMiddleware) @@ -41,14 +38,13 @@ func startHttpServer(container *Container) { auth.Handle("/timecode_likes", Handler{container, handleCreateTimecodeLike}).Methods(http.MethodPost) auth.Handle("/timecode_likes", Handler{container, handleDeleteTimecodeLike}).Methods(http.MethodDelete) - handler := cors.Default().Handler(router) - - log.Fatal(http.ListenAndServe(":8080", handler)) + return cors.Default().Handler(router) } func commonMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "application/json") + next.ServeHTTP(w, r) }) } diff --git a/cmd/timecode_likes_controller.go b/cmd/timecode_likes_controller.go index 16ec42d..0406f0a 100644 --- a/cmd/timecode_likes_controller.go +++ b/cmd/timecode_likes_controller.go @@ -16,14 +16,18 @@ func handleCreateTimecodeLike(c *Container, w http.ResponseWriter, r *http.Reque err := json.Unmarshal(reqBody, like) if err != nil { log.Println(err) + + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } _, err = c.TimecodeLikeRepository.Create(like, currentUser.ID) if err != nil { + w.WriteHeader(http.StatusUnprocessableEntity) json.NewEncoder(w).Encode(err) } else { + w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(like) } } @@ -37,13 +41,17 @@ func handleDeleteTimecodeLike(c *Container, w http.ResponseWriter, r *http.Reque err := json.Unmarshal(reqBody, timecodeLike) if err != nil { log.Println(err) + + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } _, err = c.TimecodeLikeRepository.Delete(timecodeLike, currentUser.ID) if err != nil { + w.WriteHeader(http.StatusUnprocessableEntity) json.NewEncoder(w).Encode(err) } else { + w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(timecodeLike) } } diff --git a/cmd/timecode_likes_controller_test.go b/cmd/timecode_likes_controller_test.go new file mode 100644 index 0000000..375c2df --- /dev/null +++ b/cmd/timecode_likes_controller_test.go @@ -0,0 +1,150 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const invalidTimecodeID = 1984 + +var invalidJSON = []byte("Invalid JSON") + +var mockTKRepo = &MockTimecodeLikeRepository{} +var container = &Container{ + TimecodeLikeRepository: mockTKRepo, +} +var timecodeLikeRouter = createRouter(container) + +type MockTimecodeLikeRepository struct { + mock.Mock +} + +func (mr *MockTimecodeLikeRepository) Create(like *TimecodeLike, userID uint) (*TimecodeLike, error) { + args := mr.Called(like, userID) + err := args.Error(1) + + like.UserID = userID + + if like.TimecodeID == invalidTimecodeID { + return nil, err + } + + argLike := args.Get(0).(*TimecodeLike) + argLike = like + + return argLike, nil +} + +func (mr *MockTimecodeLikeRepository) Delete(like *TimecodeLike, userID uint) (*TimecodeLike, error) { + args := mr.Called(like, userID) + err := args.Error(1) + + like.UserID = userID + + if like.TimecodeID == invalidTimecodeID { + return nil, err + } + + argLike := args.Get(0).(*TimecodeLike) + argLike = like + + return argLike, nil +} + +func Test_handleCreateTimecodeLike(t *testing.T) { + url := "/auth/timecode_likes" + + t.Run("when creation is successful", func(t *testing.T) { + currentUser := &User{} + currentUser.ID = 1 + + like := &TimecodeLike{TimecodeID: 5} + mockTKRepo.On("Create", like, currentUser.ID).Return(like, nil) + + params := []byte(`{ "timecodeId": 5 }`) + req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(params)) + + response := executeRequest(t, timecodeLikeRouter, req, currentUser) + + mockTKRepo.AssertExpectations(t) + assert.Equal(t, http.StatusCreated, response.Code) + }) + + t.Run("when creation has been failed", func(t *testing.T) { + currentUser := &User{} + currentUser.ID = 1 + + like := &TimecodeLike{TimecodeID: invalidTimecodeID} + mockTKRepo.On("Create", like, currentUser.ID).Return(nil, errors.New("")) + + params := []byte(fmt.Sprintf(`{ "timecodeId": %d }`, invalidTimecodeID)) + req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(params)) + + response := executeRequest(t, timecodeLikeRouter, req, currentUser) + + mockTKRepo.AssertExpectations(t) + assert.Equal(t, http.StatusUnprocessableEntity, response.Code) + }) + + t.Run("when invalid request params has been given", func(t *testing.T) { + currentUser := &User{} + + req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(invalidJSON)) + + response := executeRequest(t, timecodeLikeRouter, req, currentUser) + + assert.Equal(t, http.StatusBadRequest, response.Code) + }) +} + +func Test_handleDeleteTimecodeLike(t *testing.T) { + url := "/auth/timecode_likes" + + t.Run("when deletion is successful", func(t *testing.T) { + currentUser := &User{} + currentUser.ID = 1 + + like := &TimecodeLike{TimecodeID: 5} + mockTKRepo.On("Delete", like, currentUser.ID).Return(like, nil) + + params := []byte(`{ "timecodeId": 5 }`) + req, _ := http.NewRequest(http.MethodDelete, url, bytes.NewBuffer(params)) + + response := executeRequest(t, timecodeLikeRouter, req, currentUser) + + mockTKRepo.AssertExpectations(t) + assert.Equal(t, http.StatusOK, response.Code) + }) + + t.Run("when deletion has been failed", func(t *testing.T) { + currentUser := &User{} + currentUser.ID = 1 + + like := &TimecodeLike{TimecodeID: invalidTimecodeID} + mockTKRepo.On("Delete", like, currentUser.ID).Return(nil, errors.New("")) + + params := []byte(fmt.Sprintf(`{ "timecodeId": %d }`, invalidTimecodeID)) + req, _ := http.NewRequest(http.MethodDelete, url, bytes.NewBuffer(params)) + + response := executeRequest(t, timecodeLikeRouter, req, currentUser) + + mockTKRepo.AssertExpectations(t) + assert.Equal(t, http.StatusUnprocessableEntity, response.Code) + }) + + t.Run("when invalid request params has been given", func(t *testing.T) { + currentUser := &User{} + + req, _ := http.NewRequest(http.MethodDelete, url, bytes.NewBuffer(invalidJSON)) + + response := executeRequest(t, timecodeLikeRouter, req, currentUser) + + assert.Equal(t, http.StatusBadRequest, response.Code) + }) +} diff --git a/cmd/timecodes_controller.go b/cmd/timecodes_controller.go index 23cb95b..98317e4 100644 --- a/cmd/timecodes_controller.go +++ b/cmd/timecodes_controller.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "io/ioutil" + "log" "net/http" "github.com/gorilla/mux" @@ -19,7 +20,7 @@ type TimecodeJSON struct { VideoID string `json:"videoId"` } -// GET /timecodes +// GET /timecodes/{videoId} func handleGetTimecodes(c *Container, w http.ResponseWriter, r *http.Request) { currentUser := getCurrentUser(r) videoID := mux.Vars(r)["videoId"] @@ -46,9 +47,12 @@ func handleCreateTimecode(c *Container, w http.ResponseWriter, r *http.Request) reqBody, _ := ioutil.ReadAll(r.Body) err := json.Unmarshal(reqBody, timecode) if err != nil { - json.NewEncoder(w).Encode(err) + log.Println(err) + + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } + _, err = c.TimecodeRepository.Create(timecode) if err != nil { json.NewEncoder(w).Encode(err) diff --git a/cmd/timecodes_controller_test.go b/cmd/timecodes_controller_test.go new file mode 100644 index 0000000..53a445d --- /dev/null +++ b/cmd/timecodes_controller_test.go @@ -0,0 +1,106 @@ +package main + +import ( + "bytes" + "net/http" + "testing" + timecodeParser "timecodes/cmd/timecode_parser" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +var mockTimecodeRepo = &MockTimecodeRepository{} +var timecodesContainer = &Container{ + TimecodeRepository: mockTimecodeRepo, +} +var timecodesRouter = createRouter(timecodesContainer) + +type MockTimecodeRepository struct { + mock.Mock +} + +func (m *MockTimecodeRepository) FindByVideoId(videoID string) *[]*Timecode { + args := m.Called(videoID) + + collection := args.Get(0).(*[]*Timecode) + if videoID == "no-items" { + return collection + } + + collection = &[]*Timecode{{}, {}, {}} + + return collection +} + +func (m *MockTimecodeRepository) Create(timecode *Timecode) (*Timecode, error) { + args := m.Called(timecode) + + err := args.Error(1) + + if len(timecode.Description) == 0 { + return nil, err + } + + argTimecode := args.Get(0).(*Timecode) + argTimecode = timecode + + return argTimecode, nil +} + +func (m *MockTimecodeRepository) CreateFromParsedCodes(parsedCodes []timecodeParser.ParsedTimeCode, videoID string) *[]*Timecode { + args := m.Called(parsedCodes, videoID) + + return args.Get(0).(*[]*Timecode) +} + +func Test_handleGetTimecodes(t *testing.T) { + currentUser := &User{} + currentUser.ID = 1 + + t.Run("when timecodes exist", func(t *testing.T) { + timecodes := &[]*Timecode{{}, {}, {}} + + mockTimecodeRepo.On("FindByVideoId", "video-id").Return(timecodes, nil) + + req, _ := http.NewRequest(http.MethodGet, "/timecodes/video-id", nil) + + response := executeRequest(t, timecodesRouter, req, currentUser) + + mockTimecodeRepo.AssertExpectations(t) + assert.Equal(t, http.StatusOK, response.Code) + }) + + t.Run("when timecodes don't exist", func(t *testing.T) { + t.Skip() + timecodes := &[]*Timecode{} + + mockTimecodeRepo.On("FindByVideoId", "no-items").Return(timecodes, nil) + + req, _ := http.NewRequest(http.MethodGet, "/timecodes/video-id", nil) + + response := executeRequest(t, timecodesRouter, req, currentUser) + + mockTimecodeRepo.AssertExpectations(t) + assert.Equal(t, http.StatusOK, response.Code) + }) +} + +func Test_handleCreateTimecode(t *testing.T) { + currentUser := &User{} + currentUser.ID = 1 + + t.Run("when request params are valid", func(t *testing.T) { + timecode := &Timecode{VideoID: "video-id", Seconds: 111, Description: "ABC"} + + mockTimecodeRepo.On("Create", timecode).Return(timecode, nil) + + params := []byte(`{ "videoId": "video-id", "seconds": 111, "description": "ABC" }`) + req, _ := http.NewRequest(http.MethodPost, "/auth/timecodes", bytes.NewBuffer(params)) + + response := executeRequest(t, timecodesRouter, req, currentUser) + + mockTimecodeRepo.AssertExpectations(t) + assert.Equal(t, http.StatusOK, response.Code) + }) +} diff --git a/coverage.out b/coverage.out deleted file mode 100644 index 7f7f996..0000000 --- a/coverage.out +++ /dev/null @@ -1,129 +0,0 @@ -mode: set -timecodes/cmd/timecode_parser/timecode_parser.go:27.58,35.2 5 1 -timecodes/cmd/timecode_parser/timecode_parser.go:37.57,40.82 2 1 -timecodes/cmd/timecode_parser/timecode_parser.go:46.2,46.13 1 1 -timecodes/cmd/timecode_parser/timecode_parser.go:40.82,41.38 1 1 -timecodes/cmd/timecode_parser/timecode_parser.go:41.38,43.4 1 1 -timecodes/cmd/timecode_parser/timecode_parser.go:49.72,50.37 1 1 -timecodes/cmd/timecode_parser/timecode_parser.go:54.2,54.34 1 1 -timecodes/cmd/timecode_parser/timecode_parser.go:67.2,67.19 1 1 -timecodes/cmd/timecode_parser/timecode_parser.go:50.37,52.3 1 1 -timecodes/cmd/timecode_parser/timecode_parser.go:54.34,65.3 6 1 -timecodes/cmd/timecode_parser/timecode_parser.go:70.46,73.35 2 1 -timecodes/cmd/timecode_parser/timecode_parser.go:79.2,83.20 4 1 -timecodes/cmd/timecode_parser/timecode_parser.go:73.35,75.3 1 1 -timecodes/cmd/timecode_parser/timecode_parser.go:75.8,77.3 1 1 -timecodes/cmd/timecode_parser/timecode_parser.go:86.46,90.36 3 1 -timecodes/cmd/timecode_parser/timecode_parser.go:96.2,96.16 1 1 -timecodes/cmd/timecode_parser/timecode_parser.go:90.36,94.3 3 1 -timecodes/cmd/google_api/google_api.go:25.35,27.2 1 1 -timecodes/cmd/google_api/google_api.go:29.72,30.27 1 1 -timecodes/cmd/google_api/google_api.go:34.2,36.16 3 1 -timecodes/cmd/google_api/google_api.go:40.2,42.16 3 1 -timecodes/cmd/google_api/google_api.go:46.2,46.29 1 1 -timecodes/cmd/google_api/google_api.go:64.2,64.22 1 1 -timecodes/cmd/google_api/google_api.go:30.27,32.3 1 1 -timecodes/cmd/google_api/google_api.go:36.16,38.3 1 1 -timecodes/cmd/google_api/google_api.go:42.16,44.3 1 1 -timecodes/cmd/google_api/google_api.go:47.11,50.26 3 1 -timecodes/cmd/google_api/google_api.go:54.3,54.17 1 1 -timecodes/cmd/google_api/google_api.go:55.10,59.17 4 1 -timecodes/cmd/google_api/google_api.go:50.26,52.4 1 0 -timecodes/cmd/google_api/google_api.go:59.17,61.4 1 0 -timecodes/cmd/timecode_likes_controller.go:11.85,17.16 5 0 -timecodes/cmd/timecode_likes_controller.go:22.2,24.16 2 0 -timecodes/cmd/timecode_likes_controller.go:17.16,20.3 2 0 -timecodes/cmd/timecode_likes_controller.go:24.16,26.3 1 0 -timecodes/cmd/timecode_likes_controller.go:26.8,28.3 1 0 -timecodes/cmd/timecode_likes_controller.go:32.85,38.16 5 0 -timecodes/cmd/timecode_likes_controller.go:43.2,44.16 2 0 -timecodes/cmd/timecode_likes_controller.go:38.16,41.3 2 0 -timecodes/cmd/timecode_likes_controller.go:44.16,46.3 1 0 -timecodes/cmd/timecode_likes_controller.go:46.8,48.3 1 0 -timecodes/cmd/db.go:13.25,15.2 1 1 -timecodes/cmd/db.go:17.26,19.2 1 1 -timecodes/cmd/db.go:21.24,25.16 3 1 -timecodes/cmd/db.go:30.2,32.11 2 1 -timecodes/cmd/db.go:25.16,28.3 2 0 -timecodes/cmd/db.go:35.45,39.16 3 0 -timecodes/cmd/db.go:43.2,46.16 3 0 -timecodes/cmd/db.go:50.2,50.11 1 0 -timecodes/cmd/db.go:39.16,41.3 1 0 -timecodes/cmd/db.go:46.16,48.3 1 0 -timecodes/cmd/db.go:53.38,56.16 3 0 -timecodes/cmd/db.go:60.2,60.11 1 0 -timecodes/cmd/db.go:56.16,58.3 1 0 -timecodes/cmd/db.go:63.41,65.12 2 0 -timecodes/cmd/db.go:68.34,76.2 1 1 -timecodes/cmd/timecode_like_repository.go:22.110,28.2 3 1 -timecodes/cmd/timecode_like_repository.go:30.110,32.16 2 1 -timecodes/cmd/timecode_like_repository.go:36.2,38.26 2 1 -timecodes/cmd/timecode_like_repository.go:32.16,34.3 1 1 -timecodes/cmd/db_migrations.go:5.33,9.2 3 1 -timecodes/cmd/db_migrations.go:11.44,21.2 3 1 -timecodes/cmd/db_migrations.go:23.40,25.2 1 1 -timecodes/cmd/db_migrations.go:27.48,35.2 4 1 -timecodes/cmd/main.go:9.13,14.16 4 0 -timecodes/cmd/main.go:18.2,25.28 2 0 -timecodes/cmd/main.go:14.16,16.3 1 0 -timecodes/cmd/router.go:18.68,20.2 1 0 -timecodes/cmd/router.go:22.44,47.2 14 0 -timecodes/cmd/router.go:49.55,50.71 1 0 -timecodes/cmd/router.go:50.71,53.3 2 0 -timecodes/cmd/router.go:56.57,58.2 1 0 -timecodes/cmd/router.go:60.44,62.17 2 0 -timecodes/cmd/router.go:66.2,66.12 1 0 -timecodes/cmd/router.go:62.17,64.3 1 0 -timecodes/cmd/timecode_repository.go:31.78,40.2 3 1 -timecodes/cmd/timecode_repository.go:42.81,46.2 2 1 -timecodes/cmd/timecode_repository.go:48.135,52.39 3 1 -timecodes/cmd/timecode_repository.go:69.2,69.20 1 1 -timecodes/cmd/timecode_repository.go:52.39,54.29 2 1 -timecodes/cmd/timecode_repository.go:58.3,63.17 4 1 -timecodes/cmd/timecode_repository.go:66.3,66.44 1 1 -timecodes/cmd/timecode_repository.go:54.29,55.12 1 1 -timecodes/cmd/timecode_repository.go:63.17,64.12 1 1 -timecodes/cmd/timecodes_controller.go:23.79,29.26 4 0 -timecodes/cmd/timecodes_controller.go:33.2,34.38 2 0 -timecodes/cmd/timecodes_controller.go:38.2,38.51 1 0 -timecodes/cmd/timecodes_controller.go:29.26,31.3 1 0 -timecodes/cmd/timecodes_controller.go:34.38,36.3 1 0 -timecodes/cmd/timecodes_controller.go:42.81,48.16 5 0 -timecodes/cmd/timecodes_controller.go:52.2,53.16 2 0 -timecodes/cmd/timecodes_controller.go:48.16,51.3 2 0 -timecodes/cmd/timecodes_controller.go:53.16,55.3 1 0 -timecodes/cmd/timecodes_controller.go:55.8,57.3 1 0 -timecodes/cmd/timecodes_controller.go:60.92,62.24 2 0 -timecodes/cmd/timecodes_controller.go:66.2,73.3 1 0 -timecodes/cmd/timecodes_controller.go:62.24,64.3 1 0 -timecodes/cmd/timecodes_controller.go:76.59,77.29 1 0 -timecodes/cmd/timecodes_controller.go:83.2,83.14 1 0 -timecodes/cmd/timecodes_controller.go:77.29,78.28 1 0 -timecodes/cmd/timecodes_controller.go:78.28,80.4 1 0 -timecodes/cmd/timecodes_controller.go:86.72,92.35 4 0 -timecodes/cmd/timecodes_controller.go:98.2,98.66 1 0 -timecodes/cmd/timecodes_controller.go:92.35,96.3 2 0 -timecodes/cmd/auth_controller.go:8.58,10.2 1 0 -timecodes/cmd/auth_middleware.go:15.72,16.44 1 0 -timecodes/cmd/auth_middleware.go:33.2,33.8 1 0 -timecodes/cmd/auth_middleware.go:16.44,17.72 1 0 -timecodes/cmd/auth_middleware.go:17.72,21.18 3 0 -timecodes/cmd/auth_middleware.go:26.4,30.24 4 0 -timecodes/cmd/auth_middleware.go:21.18,24.5 2 0 -timecodes/cmd/auth_middleware.go:36.54,39.23 2 0 -timecodes/cmd/auth_middleware.go:43.2,45.22 2 0 -timecodes/cmd/auth_middleware.go:39.23,41.3 1 0 -timecodes/cmd/user_repository.go:26.92,34.2 3 1 -timecodes/cmd/youtube_api/youtube_api.go:18.30,20.16 2 1 -timecodes/cmd/youtube_api/youtube_api.go:24.2,24.46 1 1 -timecodes/cmd/youtube_api/youtube_api.go:20.16,22.3 1 1 -timecodes/cmd/youtube_api/youtube_api.go:27.64,34.16 3 1 -timecodes/cmd/youtube_api/youtube_api.go:40.2,41.21 2 1 -timecodes/cmd/youtube_api/youtube_api.go:45.2,45.37 1 1 -timecodes/cmd/youtube_api/youtube_api.go:34.16,38.3 2 1 -timecodes/cmd/youtube_api/youtube_api.go:41.21,43.3 1 1 -timecodes/cmd/youtube_api/youtube_api.go:48.63,59.16 4 1 -timecodes/cmd/youtube_api/youtube_api.go:65.2,65.38 1 1 -timecodes/cmd/youtube_api/youtube_api.go:69.2,69.21 1 1 -timecodes/cmd/youtube_api/youtube_api.go:59.16,63.3 2 1 -timecodes/cmd/youtube_api/youtube_api.go:65.38,67.3 1 1 From b4165e5176a826d3b1c6b8388f0bef136e7c15ec Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Tue, 19 May 2020 17:17:56 +0200 Subject: [PATCH 22/25] Fix golangci-lint issues --- cmd/timecode_likes_controller_test.go | 10 ++++------ cmd/timecodes_controller_test.go | 5 ++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/cmd/timecode_likes_controller_test.go b/cmd/timecode_likes_controller_test.go index 375c2df..5a4dfce 100644 --- a/cmd/timecode_likes_controller_test.go +++ b/cmd/timecode_likes_controller_test.go @@ -35,10 +35,9 @@ func (mr *MockTimecodeLikeRepository) Create(like *TimecodeLike, userID uint) (* return nil, err } - argLike := args.Get(0).(*TimecodeLike) - argLike = like + _ = args.Get(0).(*TimecodeLike) - return argLike, nil + return like, nil } func (mr *MockTimecodeLikeRepository) Delete(like *TimecodeLike, userID uint) (*TimecodeLike, error) { @@ -51,10 +50,9 @@ func (mr *MockTimecodeLikeRepository) Delete(like *TimecodeLike, userID uint) (* return nil, err } - argLike := args.Get(0).(*TimecodeLike) - argLike = like + _ = args.Get(0).(*TimecodeLike) - return argLike, nil + return like, nil } func Test_handleCreateTimecodeLike(t *testing.T) { diff --git a/cmd/timecodes_controller_test.go b/cmd/timecodes_controller_test.go index 53a445d..bf8dd66 100644 --- a/cmd/timecodes_controller_test.go +++ b/cmd/timecodes_controller_test.go @@ -42,10 +42,9 @@ func (m *MockTimecodeRepository) Create(timecode *Timecode) (*Timecode, error) { return nil, err } - argTimecode := args.Get(0).(*Timecode) - argTimecode = timecode + _ = args.Get(0).(*Timecode) - return argTimecode, nil + return timecode, nil } func (m *MockTimecodeRepository) CreateFromParsedCodes(parsedCodes []timecodeParser.ParsedTimeCode, videoID string) *[]*Timecode { From d3b7051f7995e900e287de1b5f641b5bd749974b Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Tue, 19 May 2020 17:22:28 +0200 Subject: [PATCH 23/25] Add test coverage report into PR checks --- .github/workflows/pr_checks.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 88d4ea7..c3a4b73 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -22,4 +22,14 @@ jobs: cp .env.example .env docker-compose build app_test - name: Run tests - run: docker-compose run app_test go test ./... -cover + run: docker-compose run app_test go test ./... -covermode=count -coverprofile=coverage.out + - name: Convert coverage to lcov + uses: jandelgado/gcov2lcov-action@v1.0.2 + with: + infile: coverage.out + outfile: coverage.lcov + - name: Coveralls + uses: coverallsapp/github-action@v1.0.2 + with: + github-token: ${{ secrets.github_token }} + path-to-lcov: coverage.lcov From 565f2b5078a65002b928822cd0c7506203f2cde4 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Tue, 19 May 2020 17:24:24 +0200 Subject: [PATCH 24/25] Fix coveralls action --- .github/workflows/pr_checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index c3a4b73..c1cba71 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -29,7 +29,7 @@ jobs: infile: coverage.out outfile: coverage.lcov - name: Coveralls - uses: coverallsapp/github-action@v1.0.2 + uses: coverallsapp/github-action@master with: github-token: ${{ secrets.github_token }} path-to-lcov: coverage.lcov From 11552807609caf124fa285535929858da38b4d08 Mon Sep 17 00:00:00 2001 From: Emil Shakirov Date: Tue, 19 May 2020 17:33:39 +0200 Subject: [PATCH 25/25] Add coveralls badge to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 49619a4..c289091 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Backend for Timecodes Chrome extension +[![Coverage Status](https://coveralls.io/repos/github/letscode-io/timecodes-api/badge.svg)](https://coveralls.io/github/letscode-io/timecodes-api) + # Development ```bash