diff --git a/internal/http-server/handlers/recipe/recipe_test.go b/internal/http-server/handlers/recipe/recipe_test.go new file mode 100644 index 0000000..e195d24 --- /dev/null +++ b/internal/http-server/handlers/recipe/recipe_test.go @@ -0,0 +1,141 @@ +package recipe_test + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "recipes/internal/domain/models" + "recipes/internal/http-server/handlers/recipe" + "recipes/internal/http-server/handlers/recipe/mocks" + "recipes/internal/lib/logger/handlers/slogdiscard" + "recipes/internal/storage" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetRecipeHandler(t *testing.T) { + // cases + cases := []struct { + name string // name of test + inputJsonPattern string // input json + recipe_id uint // recipe id to get + recipe models.Recipe // recipe data + wantRespStatus string // expected status of response + wantBody string // expected response body + respError string // expected response error + storageMockError error // mock error to return + storageTimes int // count of storage calls + }{ + { + name: "Success", + recipe_id: 1, + recipe: models.Recipe{ + ID: 1, + Title: "title", + Description: "description", + Image: "img.jpg", + CookingTime: "1 час", + ServingsNum: 1, + Calories: "1 kCal", + Ingredients: []models.RecipeIngredients{ + { + Title: "Ингредиенты", + Ingredients: []string{"Ингредиент 1", "Ингредиент 2"}, + }, + }, + Recipe_steps: []string{ + "Шаг 1", + "Шаг 2", + }, + Advices: []string{ + "1. совет", + "2. совет", + }, + Categories: []string{ + "Категория 1", + "Категория 2", + }, + }, + wantBody: "{\"status\":\"OK\",\"recipe\":{\"id\":1,\"title\":\"title\",\"desc\":\"description\",\"img\":\"img.jpg\",\"ctime\":\"1 час\",\"snum\":1,\"cal\":\"1 kCal\",\"ingredients\":[{\"title\":\"Ингредиенты\",\"ingredients\":[\"Ингредиент 1\",\"Ингредиент 2\"]}],\"recipe_steps\":[\"Шаг 1\",\"Шаг 2\"],\"advices\":[\"1. совет\",\"2. совет\"],\"categories\":[\"Категория 1\",\"Категория 2\"]}}\n", + storageTimes: 1, + wantRespStatus: "OK", + inputJsonPattern: `{"recipe_id": %d}`, + }, + { + name: "Broken JSON", + respError: "failed to decode request", + inputJsonPattern: `{"recipe_id: %d}`, + wantRespStatus: "Error", + wantBody: "{\"status\":\"Error\",\"error\":\"failed to decode request\"}\n", + storageTimes: 0, + }, + { + name: "Recipe not exists", + respError: "recipe not found", + inputJsonPattern: `{"recipe_id": %d}`, + recipe_id: 1, + wantRespStatus: "Error", + storageMockError: storage.ErrRecipeNotFound, + storageTimes: 1, + wantBody: "{\"status\":\"Error\",\"error\":\"recipe not found\"}\n", + }, + { + name: "Storage error", + respError: "failed to get recipe", + inputJsonPattern: `{"recipe_id": %d}`, + recipe_id: 1, + wantRespStatus: "Error", + storageMockError: errors.New("SOME ERROR"), + storageTimes: 1, + wantBody: "{\"status\":\"Error\",\"error\":\"failed to get recipe\"}\n", + }, + { + name: "Miss recipe_id", + inputJsonPattern: "{}", + respError: "field RecipeId is a required field", + wantRespStatus: "Error", + wantBody: "{\"status\":\"Error\",\"error\":\"field RecipeId is a required field\"}\n", + storageTimes: 0, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + // create storage mock + recipeProviderMock := mocks.NewRecipeProvider(t) + if tc.storageTimes > 0 { + recipeProviderMock.On("GetRecipe", context.Background(), tc.recipe_id).Return(tc.recipe, tc.storageMockError).Times(tc.storageTimes) + } + // create handler + handler := recipe.New(slogdiscard.NewDiscardLogger(), recipeProviderMock) + // + input := fmt.Sprintf(tc.inputJsonPattern, 1) + // http request + req, err := http.NewRequest(http.MethodGet, "/recipe", bytes.NewReader([]byte(input))) + 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 recipe.Response + require.NoError(t, json.Unmarshal([]byte(body), &resp)) + assert.Equal(t, tc.respError, resp.Error) + assert.Equal(t, tc.wantRespStatus, resp.Status) + assert.Equal(t, tc.recipe, resp.Recipe) + }) + } +} diff --git a/internal/lib/logger/handlers/slogdiscard/slogdiscard.go b/internal/lib/logger/handlers/slogdiscard/slogdiscard.go new file mode 100644 index 0000000..4f19f4e --- /dev/null +++ b/internal/lib/logger/handlers/slogdiscard/slogdiscard.go @@ -0,0 +1,40 @@ +package slogdiscard + +/* + Logger for tests with ignoring input +*/ + +import ( + "context" + "log/slog" +) + +func NewDiscardLogger() *slog.Logger { + return slog.New(NewDiscardHandler()) +} + +type DiscardHandler struct{} + +func NewDiscardHandler() *DiscardHandler { + return &DiscardHandler{} +} + +func (h *DiscardHandler) Handle(_ context.Context, _ slog.Record) error { + // Просто игнорируем запись журнала + return nil +} + +func (h *DiscardHandler) WithAttrs(_ []slog.Attr) slog.Handler { + // Возвращает тот же обработчик, так как нет атрибутов для сохранения + return h +} + +func (h *DiscardHandler) WithGroup(_ string) slog.Handler { + // Возвращает тот же обработчик, так как нет группы для сохранения + return h +} + +func (h *DiscardHandler) Enabled(_ context.Context, _ slog.Level) bool { + // Всегда возвращает false, так как запись журнала игнорируется + return false +}