From bcc3feff33a5a2177a732dfc44e6b5fb310b7d0f Mon Sep 17 00:00:00 2001 From: Henrique Rodrigues Date: Thu, 9 Mar 2017 14:35:09 -0300 Subject: [PATCH] API available offers tested --- Makefile | 2 +- api/offer_request.go | 2 +- api/offer_request_test.go | 296 ++++++++++++++++++----------------- api/validation_middleware.go | 22 --- migrations/migrations.go | 6 +- models/helpers.go | 6 +- 6 files changed, 159 insertions(+), 175 deletions(-) diff --git a/Makefile b/Makefile index 4ffa7ca..1ccacba 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ start-deps: stop-deps: @env MY_IP=${MY_IP} docker-compose --project-name offers down -test: deps unit integration acceptance test-coverage-func +test: deps unit integration test-coverage-func clear-coverage-profiles: @find . -name '*.coverprofile' -delete diff --git a/api/offer_request.go b/api/offer_request.go index 3ff8708..b82c091 100644 --- a/api/offer_request.go +++ b/api/offer_request.go @@ -59,7 +59,7 @@ func (h *OfferRequestHandler) getOffers(w http.ResponseWriter, r *http.Request) } currentTime := h.App.Clock.GetTime() - ots, err := models.GetAvailableOffers(h.App.DB, nil, gameID, playerID, currentTime, mr) + ots, err := models.GetAvailableOffers(h.App.DB, h.App.RedisClient, gameID, playerID, currentTime, mr) if err != nil { logger.WithError(err).Error("Failed to retrieve offer for player.") diff --git a/api/offer_request_test.go b/api/offer_request_test.go index cce1af8..cf13fa1 100644 --- a/api/offer_request_test.go +++ b/api/offer_request_test.go @@ -20,6 +20,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/topfreegames/offers/models" . "github.com/topfreegames/offers/testing" ) @@ -41,150 +42,155 @@ var _ = Describe("Offer Handler", func() { }) }) - // Describe("GET /available-offers", func() { - // It("should return available offers", func() { - // playerID := "player-1" - // gameID := "offers-game" - // url := fmt.Sprintf("/available-offers?player-id=%s&game-id=%s", playerID, gameID) - // request, _ := http.NewRequest("GET", url, nil) - // var jsonBody map[string][]map[string]interface{} - // - // app.Router.ServeHTTP(recorder, request) - // Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json")) - // err := json.Unmarshal(recorder.Body.Bytes(), &jsonBody) - // Expect(err).NotTo(HaveOccurred()) - // Expect(recorder.Code).To(Equal(http.StatusOK)) - // Expect(jsonBody).To(HaveKey("popup")) - // Expect(jsonBody).To(HaveKey("store")) - // popup := jsonBody["popup"] - // Expect(popup).To(HaveLen(1)) - // Expect(popup[0]).To(HaveKey("id")) - // Expect(popup[0]).To(HaveKey("productId")) - // Expect(popup[0]).To(HaveKey("contents")) - // Expect(popup[0]).To(HaveKey("metadata")) - // store := jsonBody["store"] - // Expect(store).To(HaveLen(2)) - // Expect(store[0]).To(HaveKey("id")) - // Expect(store[0]).To(HaveKey("productId")) - // Expect(store[0]).To(HaveKey("contents")) - // Expect(store[0]).To(HaveKey("metadata")) - // Expect(store[0]).To(HaveKey("expireAt")) - // Expect(store[1]).To(HaveKey("id")) - // Expect(store[1]).To(HaveKey("productId")) - // Expect(store[1]).To(HaveKey("contents")) - // Expect(store[1]).To(HaveKey("metadata")) - // Expect(store[1]).To(HaveKey("expireAt")) - // }) - // - // It("should return empty list of available offers", func() { - // playerID := "player-1" - // gameID := "non-existing-offers-game" - // url := fmt.Sprintf("/available-offers?player-id=%s&game-id=%s", playerID, gameID) - // request, _ := http.NewRequest("GET", url, nil) - // var jsonBody map[string]map[string]interface{} - // - // app.Router.ServeHTTP(recorder, request) - // Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json")) - // err := json.Unmarshal(recorder.Body.Bytes(), &jsonBody) - // Expect(err).NotTo(HaveOccurred()) - // Expect(jsonBody).To(BeEmpty()) - // Expect(recorder.Code).To(Equal(http.StatusOK)) - // }) - // - // It("should return status code 400 if player-id is not informed available offers", func() { - // gameID := "offers-game" - // url := fmt.Sprintf("/available-offers?game-id=%s", gameID) - // request, _ := http.NewRequest("GET", url, nil) - // - // app.Router.ServeHTTP(recorder, request) - // Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json")) - // Expect(recorder.Code).To(Equal(http.StatusBadRequest)) - // var obj map[string]interface{} - // err := json.Unmarshal([]byte(recorder.Body.String()), &obj) - // Expect(err).NotTo(HaveOccurred()) - // Expect(obj["code"]).To(Equal("OFF-004")) - // Expect(obj["error"]).To(Equal("The player-id parameter cannot be empty.")) - // Expect(obj["description"]).To(Equal("The player-id parameter cannot be empty")) - // - // }) - // - // It("should return status code 400 if game-id is not informed available offers", func() { - // playerID := "player-1" - // url := fmt.Sprintf("/available-offers?player-id=%s", playerID) - // request, _ := http.NewRequest("GET", url, nil) - // - // app.Router.ServeHTTP(recorder, request) - // Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json")) - // Expect(recorder.Code).To(Equal(http.StatusBadRequest)) - // var obj map[string]interface{} - // err := json.Unmarshal([]byte(recorder.Body.String()), &obj) - // Expect(err).NotTo(HaveOccurred()) - // Expect(obj["code"]).To(Equal("OFF-004")) - // Expect(obj["error"]).To(Equal("The game-id parameter cannot be empty.")) - // Expect(obj["description"]).To(Equal("The game-id parameter cannot be empty")) - // }) - // - // It("should return status code of 500 if some error occurred", func() { - // playerID := "player-1" - // gameID := "offers-game" - // url := fmt.Sprintf("/available-offers?player-id=%s&game-id=%s", playerID, gameID) - // request, _ := http.NewRequest("GET", url, nil) - // - // oldDB := app.DB - // db, err := GetTestDB() - // Expect(err).NotTo(HaveOccurred()) - // app.DB = db - // app.DB.(*runner.DB).DB.Close() // make DB connection unavailable - // app.Router.ServeHTTP(recorder, request) - // Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json")) - // - // Expect(recorder.Code).To(Equal(http.StatusInternalServerError)) - // var obj map[string]interface{} - // err = json.Unmarshal([]byte(recorder.Body.String()), &obj) - // Expect(err).NotTo(HaveOccurred()) - // Expect(obj["code"]).To(Equal("OFF-004")) - // Expect(obj["error"]).To(Equal("Failed to retrieve offer for player")) - // Expect(obj["description"]).To(Equal("sql: database is closed")) - // app.DB = oldDB // avoid errors in after each - // }) - // - // It("should not return offer after claim if offer template period has max 1", func() { - // // Create Offer by requesting it - // gameID := "limited-offers-game" - // playerID := "player-1" - // place := "store" - // url := fmt.Sprintf("/available-offers?player-id=%s&game-id=%s", playerID, gameID) - // request, _ := http.NewRequest("GET", url, nil) - // var body map[string][]*models.OfferToReturn - // - // app.Router.ServeHTTP(recorder, request) - // Expect(recorder.Code).To(Equal(http.StatusOK)) - // err := json.Unmarshal(recorder.Body.Bytes(), &body) - // Expect(err).ToNot(HaveOccurred()) - // - // // Claim the Offer - // id := body[place][0].ID - // offerReader := JSONFor(JSON{ - // "playerId": playerID, - // "gameId": gameID, - // }) - // request, _ = http.NewRequest("PUT", fmt.Sprintf("/available-offers/%s/claim", id), offerReader) - // recorder = httptest.NewRecorder() - // - // app.Router.ServeHTTP(recorder, request) - // Expect(recorder.Code).To(Equal(http.StatusOK)) - // - // // Offer must not be returned again in next Get - // request, _ = http.NewRequest("GET", url, nil) - // recorder = httptest.NewRecorder() - // app.Router.ServeHTTP(recorder, request) - // Expect(recorder.Code).To(Equal(http.StatusOK)) - // var newBody map[string][]*models.OfferToReturn - // err = json.Unmarshal(recorder.Body.Bytes(), &newBody) - // Expect(err).ToNot(HaveOccurred()) - // Expect(newBody).NotTo(HaveKey(place)) - // }) - // }) + Describe("GET /available-offers", func() { + It("should return available offers", func() { + playerID := "player-1" + gameID := "offers-game" + url := fmt.Sprintf("/available-offers?player-id=%s&game-id=%s", playerID, gameID) + request, _ := http.NewRequest("GET", url, nil) + var jsonBody map[string][]map[string]interface{} + + app.Router.ServeHTTP(recorder, request) + Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json")) + err := json.Unmarshal(recorder.Body.Bytes(), &jsonBody) + Expect(err).NotTo(HaveOccurred()) + Expect(recorder.Code).To(Equal(http.StatusOK)) + Expect(jsonBody).To(HaveKey("popup")) + Expect(jsonBody).To(HaveKey("store")) + popup := jsonBody["popup"] + Expect(popup).To(HaveLen(1)) + Expect(popup[0]).To(HaveKey("id")) + Expect(popup[0]).To(HaveKey("productId")) + Expect(popup[0]).To(HaveKey("contents")) + Expect(popup[0]).To(HaveKey("metadata")) + store := jsonBody["store"] + Expect(store).To(HaveLen(2)) + Expect(store[0]).To(HaveKey("id")) + Expect(store[0]).To(HaveKey("productId")) + Expect(store[0]).To(HaveKey("contents")) + Expect(store[0]).To(HaveKey("metadata")) + Expect(store[0]).To(HaveKey("expireAt")) + Expect(store[1]).To(HaveKey("id")) + Expect(store[1]).To(HaveKey("productId")) + Expect(store[1]).To(HaveKey("contents")) + Expect(store[1]).To(HaveKey("metadata")) + Expect(store[1]).To(HaveKey("expireAt")) + }) + + It("should return empty list of available offers", func() { + playerID := "player-1" + gameID := "non-existing-offers-game" + url := fmt.Sprintf("/available-offers?player-id=%s&game-id=%s", playerID, gameID) + request, _ := http.NewRequest("GET", url, nil) + var jsonBody map[string]map[string]interface{} + + app.Router.ServeHTTP(recorder, request) + Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json")) + err := json.Unmarshal(recorder.Body.Bytes(), &jsonBody) + Expect(err).NotTo(HaveOccurred()) + Expect(jsonBody).To(BeEmpty()) + Expect(recorder.Code).To(Equal(http.StatusOK)) + }) + + It("should return status code 400 if player-id is not informed available offers", func() { + gameID := "offers-game" + url := fmt.Sprintf("/available-offers?game-id=%s", gameID) + request, _ := http.NewRequest("GET", url, nil) + + app.Router.ServeHTTP(recorder, request) + Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json")) + Expect(recorder.Code).To(Equal(http.StatusBadRequest)) + var obj map[string]interface{} + err := json.Unmarshal([]byte(recorder.Body.String()), &obj) + Expect(err).NotTo(HaveOccurred()) + Expect(obj["code"]).To(Equal("OFF-004")) + Expect(obj["error"]).To(Equal("The player-id parameter cannot be empty.")) + Expect(obj["description"]).To(Equal("The player-id parameter cannot be empty")) + + }) + + It("should return status code 400 if game-id is not informed available offers", func() { + playerID := "player-1" + url := fmt.Sprintf("/available-offers?player-id=%s", playerID) + request, _ := http.NewRequest("GET", url, nil) + + app.Router.ServeHTTP(recorder, request) + Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json")) + Expect(recorder.Code).To(Equal(http.StatusBadRequest)) + var obj map[string]interface{} + err := json.Unmarshal([]byte(recorder.Body.String()), &obj) + Expect(err).NotTo(HaveOccurred()) + Expect(obj["code"]).To(Equal("OFF-004")) + Expect(obj["error"]).To(Equal("The game-id parameter cannot be empty.")) + Expect(obj["description"]).To(Equal("The game-id parameter cannot be empty")) + }) + + It("should return status code of 500 if some error occurred", func() { + playerID := "player-1" + gameID := "offers-game" + url := fmt.Sprintf("/available-offers?player-id=%s&game-id=%s", playerID, gameID) + request, _ := http.NewRequest("GET", url, nil) + + oldDB := app.DB + db, err := GetTestDB() + Expect(err).NotTo(HaveOccurred()) + app.DB = db + app.DB.(*runner.DB).DB.Close() // make DB connection unavailable + app.Router.ServeHTTP(recorder, request) + Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json")) + + Expect(recorder.Code).To(Equal(http.StatusInternalServerError)) + var obj map[string]interface{} + err = json.Unmarshal([]byte(recorder.Body.String()), &obj) + Expect(err).NotTo(HaveOccurred()) + Expect(obj["code"]).To(Equal("OFF-004")) + Expect(obj["error"]).To(Equal("Failed to retrieve offer for player")) + Expect(obj["description"]).To(Equal("sql: database is closed")) + app.DB = oldDB // avoid errors in after each + }) + + It("should not return offer after claim if offer template period has max 1", func() { + // Create Offer by requesting it + gameID := "limited-offers-game" + playerID := "player-1" + place := "store" + url := fmt.Sprintf("/available-offers?player-id=%s&game-id=%s", playerID, gameID) + request, _ := http.NewRequest("GET", url, nil) + var body map[string][]*models.OfferToReturn + + app.Router.ServeHTTP(recorder, request) + Expect(recorder.Code).To(Equal(http.StatusOK)) + err := json.Unmarshal(recorder.Body.Bytes(), &body) + Expect(err).ToNot(HaveOccurred()) + + // Claim the Offer + id := body[place][0].ID + offerReader := JSONFor(JSON{ + "gameId": gameID, + "playerId": playerID, + "productId": "com.tfg.sample", + "timestamp": time.Now().Unix(), + "transactionId": uuid.NewV4().String(), + "offerInstanceId": id, + }) + request, _ = http.NewRequest("PUT", "/offers/claim", offerReader) + recorder = httptest.NewRecorder() + + app.Router.ServeHTTP(recorder, request) + Expect(recorder.Body.String()).To(Equal(`{"contents":{"gems":5,"gold":100}}`)) + Expect(recorder.Code).To(Equal(http.StatusOK)) + + // Offer must not be returned again in next Get + request, _ = http.NewRequest("GET", url, nil) + recorder = httptest.NewRecorder() + app.Router.ServeHTTP(recorder, request) + Expect(recorder.Code).To(Equal(http.StatusOK)) + var newBody map[string][]*models.OfferToReturn + err = json.Unmarshal(recorder.Body.Bytes(), &newBody) + Expect(err).ToNot(HaveOccurred()) + Expect(newBody).NotTo(HaveKey(place)) + }) + }) Describe("PUT /offers/claim", func() { It("should claim valid offer", func() { @@ -284,7 +290,7 @@ var _ = Describe("Offer Handler", func() { Expect(err).NotTo(HaveOccurred()) Expect(obj["code"]).To(Equal("OFF-002")) Expect(obj["error"]).To(Equal("ValidationFailedError")) - Expect(obj["description"]).To(Equal("GameID: non zero value required;PlayerID: non zero value required;ProductID: non zero value required;Timestamp: 0 does not validate as requiredInt;;TransactionID: non zero value required;")) + Expect(obj["description"]).To(Equal("GameID: non zero value required;PlayerID: non zero value required;ProductID: non zero value required;Timestamp: non zero value required;TransactionID: non zero value required;")) }) It("should return 404 if non existing OfferID", func() { diff --git a/api/validation_middleware.go b/api/validation_middleware.go index 6f76c9c..b13e694 100644 --- a/api/validation_middleware.go +++ b/api/validation_middleware.go @@ -11,7 +11,6 @@ import ( "context" "encoding/json" "net/http" - "strconv" "gopkg.in/mgutz/dat.v2/dat" @@ -111,27 +110,6 @@ func (m *ValidationMiddleware) configureCustomValidators() { }, ), ) - govalidator.CustomTypeTagMap.Set( - "requiredInt", - govalidator.CustomTypeValidator( - func(i interface{}, context interface{}) bool { - switch v := i.(type) { - case string: - _, err := strconv.ParseInt(v, 10, 64) - return err == nil && v != "" - case int: - return v != 0 - case int16: - return v != int16(0) - case int32: - return v != int32(0) - case int64: - return v != int64(0) - } - return false - }, - ), - ) } //ServeHTTP method diff --git a/migrations/migrations.go b/migrations/migrations.go index 41de5e1..e3c4f6a 100644 --- a/migrations/migrations.go +++ b/migrations/migrations.go @@ -85,7 +85,7 @@ func migrations0001CreategamestableSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "migrations/0001-CreateGamesTable.sql", size: 292, mode: os.FileMode(420), modTime: time.Unix(1488915325, 0)} + info := bindataFileInfo{name: "migrations/0001-CreateGamesTable.sql", size: 292, mode: os.FileMode(420), modTime: time.Unix(1488938696, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -105,7 +105,7 @@ func migrations0002CreateofferstableSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "migrations/0002-CreateOffersTable.sql", size: 645, mode: os.FileMode(420), modTime: time.Unix(1488997322, 0)} + info := bindataFileInfo{name: "migrations/0002-CreateOffersTable.sql", size: 645, mode: os.FileMode(420), modTime: time.Unix(1488998448, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -125,7 +125,7 @@ func migrations0003CreateofferintancestableSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "migrations/0003-CreateOfferIntancesTable.sql", size: 597, mode: os.FileMode(420), modTime: time.Unix(1488990082, 0)} + info := bindataFileInfo{name: "migrations/0003-CreateOfferIntancesTable.sql", size: 597, mode: os.FileMode(420), modTime: time.Unix(1488990236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/models/helpers.go b/models/helpers.go index eb206f5..3a79ed4 100644 --- a/models/helpers.go +++ b/models/helpers.go @@ -25,9 +25,9 @@ import ( type ClaimOfferPayload struct { GameID string `json:"gameId" valid:"matches(^[^-][a-z0-9-]*$),stringlength(1|255),required"` PlayerID string `json:"playerId" valid:"ascii,stringlength(1|1000),required"` - ProductID string `json:"productId" valid:"ascii,stringlength(1|1000),required"` - Timestamp int64 `json:"timestamp" valid:"requiredInt,required"` - TransactionID string `json:"transactionId" valid:"ascii,stringlength(1|1000),required"` + ProductID string `json:"productId" valid:"ascii,stringlength(1|255),required"` + Timestamp int64 `json:"timestamp" valid:"int64,required"` + TransactionID string `json:"transactionId" valid:"uuidv4,required"` OfferInstanceID string `json:"id" valid:"uuidv4,optional"` }