diff --git a/internal/http-server/handlers/recipe/recipe_test.go b/internal/http-server/handlers/recipe/recipe_test.go index e195d24..c03f219 100644 --- a/internal/http-server/handlers/recipe/recipe_test.go +++ b/internal/http-server/handlers/recipe/recipe_test.go @@ -117,7 +117,7 @@ func TestGetRecipeHandler(t *testing.T) { } // create handler handler := recipe.New(slogdiscard.NewDiscardLogger(), recipeProviderMock) - // + // create input input := fmt.Sprintf(tc.inputJsonPattern, 1) // http request req, err := http.NewRequest(http.MethodGet, "/recipe", bytes.NewReader([]byte(input))) diff --git a/internal/http-server/handlers/recipes/recipes.go b/internal/http-server/handlers/recipes/recipes.go index 7669067..796dcd2 100644 --- a/internal/http-server/handlers/recipes/recipes.go +++ b/internal/http-server/handlers/recipes/recipes.go @@ -27,7 +27,7 @@ type RecipesProvider interface { GetRecipes(ctx context.Context, offset, limit int) ([]models.Recipe, error) } -const getRecipesLimit int = 16 +const GetRecipesLimit int = 16 func New(log *slog.Logger, recipesProvider RecipesProvider) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -56,7 +56,7 @@ func New(log *slog.Logger, recipesProvider RecipesProvider) http.HandlerFunc { return } // get from storage - recipes, err := recipesProvider.GetRecipes(r.Context(), getRecipesLimit*(int(req.Page)-1), getRecipesLimit) + recipes, err := recipesProvider.GetRecipes(r.Context(), GetRecipesLimit*(int(req.Page)-1), GetRecipesLimit) if err != nil { log.Error("failed to get recipes from storage", sl.Err(err)) render.JSON(w, r, resp.Error("failed to get recipes")) diff --git a/internal/http-server/handlers/recipes/recipes_test.go b/internal/http-server/handlers/recipes/recipes_test.go new file mode 100644 index 0000000..faf2c01 --- /dev/null +++ b/internal/http-server/handlers/recipes/recipes_test.go @@ -0,0 +1,124 @@ +package recipes_test + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "recipes/internal/domain/models" + "recipes/internal/http-server/handlers/recipes" + "recipes/internal/http-server/handlers/recipes/mocks" + "recipes/internal/lib/logger/handlers/slogdiscard" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetRecipesHandler(t *testing.T) { + // cases + cases := []struct { + name string // name of test + page uint // number of page to get + inputBody string // input json + wantBody string // expected output json + wantRespError string // expected error + wantRespStatus string // expected status + storageMockError error // mock error to return + storageTimes int // count of storage calls + recipes []models.Recipe // returned data + }{ + { + name: "Success", + page: 1, + inputBody: `{"page": 1}`, + wantBody: "{\"status\":\"OK\",\"recipes\":[{\"id\":1,\"title\":\"title 1\",\"img\":\"1.jpg\",\"ctime\":\"1 час\",\"cal\":\"100 kCal\"},{\"id\":2,\"title\":\"title 2\",\"img\":\"2.jpg\",\"ctime\":\"2 час\",\"cal\":\"200 kCal\"}]}\n", + wantRespStatus: "OK", + storageTimes: 1, + recipes: []models.Recipe{ + { + ID: 1, + Title: "title 1", + Image: "1.jpg", + CookingTime: "1 час", + Calories: "100 kCal", + }, + { + ID: 2, + Title: "title 2", + Image: "2.jpg", + CookingTime: "2 час", + Calories: "200 kCal", + }, + }, + }, + { + name: "Empty page", + page: 10, + inputBody: `{"page": 10}`, + wantBody: "{\"status\":\"OK\",\"recipes\":null}\n", + wantRespStatus: "OK", + storageTimes: 1, + }, + { + name: "Bad data", + inputBody: `{"page": 0}`, + wantBody: "{\"status\":\"Error\",\"error\":\"field Page is a required field\"}\n", + wantRespStatus: "Error", + wantRespError: "field Page is a required field", + storageTimes: 0, + }, + { + name: "Broken JSON", + inputBody: `{"page" 1}`, + wantBody: "{\"status\":\"Error\",\"error\":\"failed to decode request\"}\n", + wantRespError: "failed to decode request", + wantRespStatus: "Error", + storageTimes: 0, + }, + { + name: "Storage error", + inputBody: `{"page": 1}`, + page: 1, + wantBody: "{\"status\":\"Error\",\"error\":\"failed to get recipes\"}\n", + wantRespError: "failed to get recipes", + wantRespStatus: "Error", + storageMockError: errors.New("SOME ERROR"), + storageTimes: 1, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + // create storage mock + recipeProviderMock := mocks.NewRecipesProvider(t) + if tc.storageTimes > 0 { + recipeProviderMock.On("GetRecipes", context.Background(), recipes.GetRecipesLimit*(int(tc.page)-1), recipes.GetRecipesLimit).Return(tc.recipes, tc.storageMockError).Times(tc.storageTimes) + } + // create handler + handler := recipes.New(slogdiscard.NewDiscardLogger(), recipeProviderMock) + // http request + req, err := http.NewRequest(http.MethodGet, "/recipes_page", bytes.NewReader([]byte(tc.inputBody))) + require.NoError(t, err) + // create request + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + // compare expected and actual + assert.Equal(t, http.StatusOK, rr.Code) + + body := rr.Body.String() + assert.Equal(t, tc.wantBody, body) + + var resp recipes.Response + require.NoError(t, json.Unmarshal([]byte(body), &resp)) + assert.Equal(t, tc.wantRespError, resp.Error) + assert.Equal(t, tc.wantRespStatus, resp.Status) + assert.Equal(t, tc.recipes, resp.Recipes) + }) + } +}