diff --git a/.github/workflows/mertricstest.yml b/.github/workflows/mertricstest.yml index 5f71aca..7d1b03c 100644 --- a/.github/workflows/mertricstest.yml +++ b/.github/workflows/mertricstest.yml @@ -78,8 +78,7 @@ jobs: github.head_ref == 'iter11' || github.head_ref == 'iter12' || github.head_ref == 'iter13' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | metricstest -test.v -test.run=^TestIteration1$ \ -binary-path=cmd/server/server @@ -104,8 +103,7 @@ jobs: github.head_ref == 'iter11' || github.head_ref == 'iter12' || github.head_ref == 'iter13' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | metricstest -test.v -test.run=^TestIteration2[AB]*$ \ -source-path=. \ @@ -125,8 +123,7 @@ jobs: github.head_ref == 'iter11' || github.head_ref == 'iter12' || github.head_ref == 'iter13' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | metricstest -test.v -test.run=^TestIteration3[AB]*$ \ -source-path=. \ @@ -146,8 +143,7 @@ jobs: github.head_ref == 'iter11' || github.head_ref == 'iter12' || github.head_ref == 'iter13' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | SERVER_PORT=$(random unused-port) ADDRESS="localhost:${SERVER_PORT}" @@ -170,8 +166,7 @@ jobs: github.head_ref == 'iter11' || github.head_ref == 'iter12' || github.head_ref == 'iter13' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | SERVER_PORT=$(random unused-port) ADDRESS="localhost:${SERVER_PORT}" @@ -193,8 +188,7 @@ jobs: github.head_ref == 'iter11' || github.head_ref == 'iter12' || github.head_ref == 'iter13' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | SERVER_PORT=$(random unused-port) ADDRESS="localhost:${SERVER_PORT}" @@ -215,8 +209,7 @@ jobs: github.head_ref == 'iter11' || github.head_ref == 'iter12' || github.head_ref == 'iter13' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | SERVER_PORT=$(random unused-port) ADDRESS="localhost:${SERVER_PORT}" @@ -236,8 +229,7 @@ jobs: github.head_ref == 'iter11' || github.head_ref == 'iter12' || github.head_ref == 'iter13' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | SERVER_PORT=$(random unused-port) ADDRESS="localhost:${SERVER_PORT}" @@ -256,8 +248,7 @@ jobs: github.head_ref == 'iter11' || github.head_ref == 'iter12' || github.head_ref == 'iter13' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | SERVER_PORT=$(random unused-port) ADDRESS="localhost:${SERVER_PORT}" @@ -276,8 +267,7 @@ jobs: github.head_ref == 'iter11' || github.head_ref == 'iter12' || github.head_ref == 'iter13' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | SERVER_PORT=$(random unused-port) ADDRESS="localhost:${SERVER_PORT}" @@ -295,8 +285,7 @@ jobs: github.head_ref == 'iter11' || github.head_ref == 'iter12' || github.head_ref == 'iter13' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | SERVER_PORT=$(random unused-port) ADDRESS="localhost:${SERVER_PORT}" @@ -313,8 +302,7 @@ jobs: github.ref == 'refs/heads/main' || github.head_ref == 'iter12' || github.head_ref == 'iter13' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | SERVER_PORT=$(random unused-port) ADDRESS="localhost:${SERVER_PORT}" @@ -330,8 +318,7 @@ jobs: if: | github.ref == 'refs/heads/main' || github.head_ref == 'iter13' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | SERVER_PORT=$(random unused-port) ADDRESS="localhost:${SERVER_PORT}" @@ -346,8 +333,7 @@ jobs: - name: "Code increment #14" if: | github.ref == 'refs/heads/main' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | SERVER_PORT=$(random unused-port) ADDRESS="localhost:${SERVER_PORT}" @@ -363,7 +349,6 @@ jobs: - name: "Code increment #14 (race detection)" if: | github.ref == 'refs/heads/main' || - github.head_ref == 'iter14' || - github.head_ref == 'iter15' + github.head_ref == 'iter14' run: | go test -v -race ./... diff --git a/Makefile b/Makefile index 79c2a0c..2e0a348 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,12 @@ -postgres: - docker run --name metric_db -e POSTGRES_USER=metric_db -e POSTGRES_PASSWORD=metric_db -p 5434:5432 -d postgres:alpine -postgresrm: - docker stop metric_db - docker rm metric_db - -migrateup: - migrate -path internal/adapter/db/postgres/migration -database "postgres://metric_db:metric_db@localhost:5434/metric_db?sslmode=disable" -verbose up - -migratedown: - migrate -path internal/adapter/db/postgres/migration -database "postgres://metric_db:metric_db@localhost:5434/metric_db?sslmode=disable" -verbose down +postgres: + docker run --name metric_db -e POSTGRES_USER=metric_db -e POSTGRES_PASSWORD=metric_db -p 5434:5432 -d postgres:alpine +postgresrm: + docker stop metric_db + docker rm metric_db + +migrateup: + migrate -path internal/adapter/db/postgres/migration -database "postgres://metric_db:metric_db@localhost:5434/metric_db?sslmode=disable" -verbose up + +migratedown: + migrate -path internal/adapter/db/postgres/migration -database "postgres://metric_db:metric_db@localhost:5434/metric_db?sslmode=disable" -verbose down .PHONY: postgres postgresrm migrateup migratedown \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index 280784c..ff604c4 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -9,25 +9,126 @@ import ( "os/signal" "sync" "syscall" - "time" + // "time" _ "net/http/pprof" - "github.com/The-Gleb/go_metrics_and_alerting/internal/app" - "github.com/The-Gleb/go_metrics_and_alerting/internal/filestorage" - "github.com/The-Gleb/go_metrics_and_alerting/internal/handlers" + // "github.com/The-Gleb/go_metrics_and_alerting/internal/app" + // "github.com/The-Gleb/go_metrics_and_alerting/internal/compressor" + v1 "github.com/The-Gleb/go_metrics_and_alerting/internal/controller/http/v1/handler" + "github.com/The-Gleb/go_metrics_and_alerting/internal/controller/http/v1/middleware" + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/service" + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/usecase" + // "github.com/The-Gleb/go_metrics_and_alerting/internal/filestorage" + // "github.com/The-Gleb/go_metrics_and_alerting/internal/handlers" "github.com/The-Gleb/go_metrics_and_alerting/internal/logger" - "github.com/The-Gleb/go_metrics_and_alerting/internal/repositories" - "github.com/The-Gleb/go_metrics_and_alerting/internal/repositories/database" - "github.com/The-Gleb/go_metrics_and_alerting/internal/repositories/memory" - "github.com/The-Gleb/go_metrics_and_alerting/internal/server" - "github.com/The-Gleb/go_metrics_and_alerting/pkg/utils/retry" + // "github.com/The-Gleb/go_metrics_and_alerting/internal/repositories" + "github.com/The-Gleb/go_metrics_and_alerting/internal/repository/database" + "github.com/The-Gleb/go_metrics_and_alerting/internal/repository/memory" + // "github.com/The-Gleb/go_metrics_and_alerting/internal/repository" + "github.com/The-Gleb/go_metrics_and_alerting/internal/repository/file_storage" + // "github.com/The-Gleb/go_metrics_and_alerting/internal/server" + // "github.com/The-Gleb/go_metrics_and_alerting/pkg/utils/retry" + "github.com/go-chi/chi/v5" ) // postgres://metric_db:metric_db@localhost:5434/metric_db?sslmode=disable // TODO: fix status in logger func main() { + + if err := Run(); err != nil { + log.Fatal(err) + } + + // go func() { + // log.Println(http.ListenAndServe("localhost:6060", nil)) + // }() + + // config := NewConfigFromFlags() + + // if err := logger.Initialize(config.LogLevel); err != nil { + // logger.Log.Fatal(err) + // return + // } + // logger.Log.Info(config) + + // var repository repositories.Repositiries + // var fileStorage app.FileStorage + + // if config.FileStoragePath != "" { + // repository = memory.New() + // fileStorage = filestorage.NewFileStorage(config.FileStoragePath, config.StoreInterval, config.Restore) + // } + + // if config.DatabaseDSN != "" { + // var db *database.DB + // var err error + // err = retry.DefaultRetry( + // context.Background(), + // func(ctx context.Context) error { + // db, err = database.ConnectDB(config.DatabaseDSN) + // return err + // }, + // ) + + // if err != nil { + // logger.Log.Fatal(err) + // return + // } + // repository = db + // } + + // app := app.NewApp(repository, fileStorage) + // handlers := handlers.New(app) + // s := server.NewWithProfiler(config.Addres, handlers, []byte(config.SignKey)) + + // if config.Restore { + // app.LoadDataFromFile(context.Background()) + // } + + // var wg sync.WaitGroup + // ctx, cancel := context.WithCancel(context.Background()) + + // if config.StoreInterval > 0 && config.DatabaseDSN == "" { + // saveTicker := time.NewTicker(time.Duration(config.StoreInterval) * time.Second) + // wg.Add(1) + // go func() { + // defer wg.Done() + // for { + // select { + // case <-saveTicker.C: + // app.StoreDataToFile(context.Background()) + // case <-ctx.Done(): + // logger.Log.Debug("stop saving to file") + // return + // } + // } + + // }() + // } + + // wg.Add(1) + // go func() { + // defer wg.Done() + // ServerShutdownSignal := make(chan os.Signal, 1) + // signal.Notify(ServerShutdownSignal, syscall.SIGINT) + // <-ServerShutdownSignal + // s.Shutdown(context.Background()) + // cancel() + // }() + + // err := server.Run(s) + // if err != nil && err != http.ErrServerClosed { + // panic(err) + // } + // os. + // wg.Wait() + // logger.Log.Info("server shutdown") +} + +func Run() error { + go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() @@ -36,64 +137,64 @@ func main() { if err := logger.Initialize(config.LogLevel); err != nil { logger.Log.Fatal(err) - return + return err } logger.Log.Info(config) - var repository repositories.Repositiries - var fileStorage app.FileStorage + var repository service.MetricStorage + var fileStorage service.FileStorage if config.FileStoragePath != "" { - repository = memory.New() - fileStorage = filestorage.NewFileStorage(config.FileStoragePath, config.StoreInterval, config.Restore) + fileStorage = file_storage.NewFileStorage(config.FileStoragePath) } if config.DatabaseDSN != "" { - var db *database.DB - var err error - err = retry.DefaultRetry( - context.Background(), - func(ctx context.Context) error { - db, err = database.ConnectDB(config.DatabaseDSN) - return err - }, - ) + db, err := database.ConnectDB(config.DatabaseDSN) if err != nil { logger.Log.Fatal(err) - return + return err } repository = db + } else { + repository = memory.New() } - app := app.NewApp(repository, fileStorage) - handlers := handlers.New(app) - s := server.NewWithProfiler(config.Addres, handlers, []byte(config.SignKey)) - - if config.Restore { - app.LoadDataFromFile(context.Background()) + metricServie := service.NewMetricService(repository) + backupService := service.NewBackupService(repository, fileStorage, config.StoreInterval, config.Restore) + + updateMetricUsecase := usecase.NewUpdateMetricUsecase(metricServie, backupService) + updateMetricSetUsecase := usecase.NewUpdateMetricSetUsecase(metricServie, backupService) + getMetricUsecase := usecase.NewGetMetricUsecase(metricServie) + getAllMetricsUsecase := usecase.NewGetAllMetricsUsecase(metricServie) + + updateMetricHandler := v1.NewUpdateMetricHandler(updateMetricUsecase) + updateMetricJSONHandler := v1.NewUpdateMetricJSONHandler(updateMetricUsecase) + getMetricHandler := v1.NewGetMetricHandler(getMetricUsecase) + getMetricJSONHandler := v1.NewGetMetricJSONHandler(getMetricUsecase) + updateMetricSetHandler := v1.NewUpdateMetricSetHandler(updateMetricSetUsecase) + getAllMetricsHandler := v1.NewGetAllMetricsHandler(getAllMetricsUsecase) + + gzipMiddleware := middleware.NewGzipMiddleware() + checkSignatureMiddleware := middleware.NewCheckSignatureMiddleware([]byte(config.SignKey)) + loggerMidleware := middleware.NewLoggerMiddleware(logger.Log) + + r := chi.NewMux() + r.Use(gzipMiddleware.Do, checkSignatureMiddleware.Do, loggerMidleware.Do) + + updateMetricHandler.AddToRouter(r) + updateMetricJSONHandler.AddToRouter(r) + getMetricHandler.AddToRouter(r) + getMetricJSONHandler.AddToRouter(r) + updateMetricSetHandler.AddToRouter(r) + getAllMetricsHandler.AddToRouter(r) + + s := http.Server{ + Addr: config.Addres, + Handler: r, } var wg sync.WaitGroup - ctx, cancel := context.WithCancel(context.Background()) - - if config.StoreInterval > 0 && config.DatabaseDSN == "" { - saveTicker := time.NewTicker(time.Duration(config.StoreInterval) * time.Second) - wg.Add(1) - go func() { - defer wg.Done() - for { - select { - case <-saveTicker.C: - app.StoreDataToFile(context.Background()) - case <-ctx.Done(): - logger.Log.Debug("stop saving to file") - return - } - } - - }() - } wg.Add(1) go func() { @@ -101,15 +202,17 @@ func main() { ServerShutdownSignal := make(chan os.Signal, 1) signal.Notify(ServerShutdownSignal, syscall.SIGINT) <-ServerShutdownSignal - s.Shutdown(context.Background()) - cancel() + err := s.Shutdown(context.Background()) + if err != nil { + panic(err) + } + logger.Log.Info("server shutdown") }() - err := server.Run(s) - if err != nil && err != http.ErrServerClosed { - panic(err) + logger.Log.Info("starting server") + if err := s.ListenAndServe(); err != nil { + logger.Log.Error("server error", "error", err) } - wg.Wait() - logger.Log.Info("server shutdown") + return nil } diff --git a/internal/app/app.go b/internal/app/app.go index a029203..74e9c79 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -11,7 +11,7 @@ import ( "github.com/The-Gleb/go_metrics_and_alerting/internal/logger" "github.com/The-Gleb/go_metrics_and_alerting/internal/models" - "github.com/The-Gleb/go_metrics_and_alerting/internal/repositories" + "github.com/The-Gleb/go_metrics_and_alerting/internal/repository" "github.com/The-Gleb/go_metrics_and_alerting/pkg/utils/retry" ) @@ -26,12 +26,12 @@ type FileStorage interface { } type app struct { - storage repositories.Repositiries + storage repository.Repositiries fileStorage FileStorage } // TODO: add FileWriter -func NewApp(s repositories.Repositiries, fs FileStorage) *app { +func NewApp(s repository.Repositiries, fs FileStorage) *app { return &app{ storage: s, fileStorage: fs, diff --git a/internal/controller/http/handler.go b/internal/controller/http/handler.go new file mode 100644 index 0000000..cd5f8e2 --- /dev/null +++ b/internal/controller/http/handler.go @@ -0,0 +1,12 @@ +package http + +import ( + "net/http" + + "github.com/go-chi/chi/v5" +) + +type Handler interface { + AddToRouter(*chi.Mux) + Middlewares(md ...func(http.Handler) http.Handler) *Handler +} diff --git a/internal/controller/http/middleware.go b/internal/controller/http/middleware.go new file mode 100644 index 0000000..bca8564 --- /dev/null +++ b/internal/controller/http/middleware.go @@ -0,0 +1,7 @@ +package http + +import "net/http" + +type Middleware interface { + Handler(http.Handler) http.Handler +} diff --git a/internal/controller/http/v1/handler/get_all_metrics.go b/internal/controller/http/v1/handler/get_all_metrics.go new file mode 100644 index 0000000..0ef67d8 --- /dev/null +++ b/internal/controller/http/v1/handler/get_all_metrics.go @@ -0,0 +1,56 @@ +package v1 + +import ( + "context" + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" +) + +const ( + getAllMetricsURL = "/" +) + +type GetAllMetricsUsecase interface { + GetAllMetricsJSON(ctx context.Context) ([]byte, error) + GetAllMetricsHTML(ctx context.Context) ([]byte, error) +} + +type getAllMetricsHandler struct { + usecase GetAllMetricsUsecase + middlewares []func(http.Handler) http.Handler +} + +func NewGetAllMetricsHandler(usecase GetAllMetricsUsecase) *getAllMetricsHandler { + return &getAllMetricsHandler{ + usecase: usecase, + middlewares: make([]func(http.Handler) http.Handler, 0), + } +} + +func (h *getAllMetricsHandler) AddToRouter(r *chi.Mux) { + r.Route(getAllMetricsURL, func(r chi.Router) { + r.Use(h.middlewares...) + r.Get("/", h.ServeHTTP) + }) +} + +func (h *getAllMetricsHandler) Middlewares(md ...func(http.Handler) http.Handler) *getAllMetricsHandler { + h.middlewares = append(h.middlewares, md...) + return h +} + +func (h *getAllMetricsHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + + body, err := h.usecase.GetAllMetricsJSON(r.Context()) + if err != nil { + err = fmt.Errorf("getAllMetricsHandler: %w", err) // TODO: handler errors + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + rw.Header().Set("Content-Type", "application/json") + rw.Write(body) + +} diff --git a/internal/controller/http/v1/handler/get_all_metrics_test.go b/internal/controller/http/v1/handler/get_all_metrics_test.go new file mode 100644 index 0000000..88a7383 --- /dev/null +++ b/internal/controller/http/v1/handler/get_all_metrics_test.go @@ -0,0 +1,132 @@ +package v1 + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "os" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/entity" + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/service" + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/usecase" + "github.com/The-Gleb/go_metrics_and_alerting/internal/repository/memory" + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/require" +) + +func testRequest(t *testing.T, ts *httptest.Server, method, + path string, body []byte) (*http.Response, string) { + req, err := http.NewRequest(method, ts.URL+path, bytes.NewReader(body)) + require.NoError(t, err) + + resp, err := ts.Client().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + return resp, string(respBody) +} + +func Test_getAllMetricHandler_ServeHTTP(t *testing.T) { + var val1 float64 = 3782369280 + var val2 int64 = 123 + metrics := []entity.Metric{ + { + MType: "gauge", + ID: "HeapAlloc", + Value: &val1, + }, + { + MType: "counter", + ID: "PollCount", + Delta: &val2, + }, + } + + metricMaps := entity.MetricsMaps{ + Gauge: metrics[:1], + Counter: metrics[1:], + } + + jsonMetrics, err := json.Marshal(metricMaps) + require.NoError(t, err) + + s := memory.New() + metricService := service.NewMetricService(s) + metricService.UpdateMetricSet(context.Background(), metrics) + getAllMetricsUsecase := usecase.NewGetAllMetricsUsecase(metricService) + getAllMetricsHandler := NewGetAllMetricsHandler(getAllMetricsUsecase) + + router := chi.NewRouter() + getAllMetricsHandler.AddToRouter(router) + ts := httptest.NewServer(router) + defer ts.Close() + + // validJsonBody := `[{"id": "HeapAlloc","type": "gauge","value": 3782369280},{"id": "PollCount","type": "counter","delta": 123}]` + + type want struct { + code int + } + tests := []struct { + name string + want want + }{ + { + name: "normal", + want: want{200}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + resp, b := testRequest(t, ts, "GET", "/", nil) + defer resp.Body.Close() + + t.Log(b) + + require.Equal(t, tt.want.code, resp.StatusCode) + if tt.want.code != 200 { + return + } + + require.Equal(t, string(jsonMetrics), b) + }) + } +} + +func Example_getAllMetricHandler_ServeHTTP() { + + s := memory.New() + s.UpdateMetric("gauge", "Alloc", "123.4") + s.UpdateMetric("counter", "PollCount", "12") + metricService := service.NewMetricService(s) + getAllMetricsUsecase := usecase.NewGetAllMetricsUsecase(metricService) + getAllMetricsHandler := NewGetAllMetricsHandler(getAllMetricsUsecase) + + router := chi.NewRouter() + getAllMetricsHandler.AddToRouter(router) + ts := httptest.NewServer(router) + defer ts.Close() + + req, _ := http.NewRequest("GET", ts.URL+"/", nil) + + resp, _ := ts.Client().Do(req) + + fmt.Println(resp.StatusCode) + b, _ := io.ReadAll(resp.Body) + fmt.Fprintln(os.Stdout, []any{string(b)}...) + + // Output: + // 200 + // {"Gauge":[{"id":"Alloc","type":"gauge","value":123.4}],"Counter":[{"id":"PollCount","type":"counter","delta":12}]} + +} diff --git a/internal/controller/http/v1/handler/get_metric.go b/internal/controller/http/v1/handler/get_metric.go new file mode 100644 index 0000000..735fae4 --- /dev/null +++ b/internal/controller/http/v1/handler/get_metric.go @@ -0,0 +1,86 @@ +package v1 + +import ( + "context" + "errors" + "net/http" + "strconv" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/entity" + "github.com/The-Gleb/go_metrics_and_alerting/internal/repository" + "github.com/go-chi/chi/v5" +) + +const ( + getMetricURL = "/value/{type}/{name}" +) + +type GetMetricUsecase interface { + GetMetric(ctx context.Context, metrics entity.Metric) (entity.Metric, error) +} + +type getMetricHandler struct { + usecase GetMetricUsecase + middlewares []func(http.Handler) http.Handler +} + +func NewGetMetricHandler(usecase GetMetricUsecase) *getMetricHandler { + return &getMetricHandler{ + usecase: usecase, + middlewares: make([]func(http.Handler) http.Handler, 0), + } +} + +func (h *getMetricHandler) AddToRouter(r *chi.Mux) { + r.Route(getMetricURL, func(r chi.Router) { + r.Use(h.middlewares...) + r.Get("/", h.ServeHTTP) + }) +} + +func (h *getMetricHandler) Middlewares(md ...func(http.Handler) http.Handler) *getMetricHandler { + h.middlewares = append(h.middlewares, md...) + return h +} + +func (h *getMetricHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + + mType := chi.URLParam(r, "type") + mName := chi.URLParam(r, "name") + metric := entity.Metric{ + MType: mType, + ID: mName, + } + + metric, err := h.usecase.GetMetric(r.Context(), metric) + if err != nil { + if errors.Is(err, repository.ErrNotFound) { + rw.WriteHeader(http.StatusNotFound) + http.Error(rw, err.Error(), http.StatusNotFound) + return + } else { + rw.WriteHeader(http.StatusBadRequest) + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + } + + switch mType { + case "gauge": + + strVal := strconv.FormatFloat(*metric.Value, 'g', -1, 64) + rw.Write([]byte(strVal)) + + case "counter": + + strVal := strconv.FormatInt(*metric.Delta, 10) + rw.Write([]byte(strVal)) + + default: + + http.Error(rw, "invalid metric type", http.StatusBadRequest) + return + + } + +} diff --git a/internal/controller/http/v1/handler/get_metric_json.go b/internal/controller/http/v1/handler/get_metric_json.go new file mode 100644 index 0000000..f3adc39 --- /dev/null +++ b/internal/controller/http/v1/handler/get_metric_json.go @@ -0,0 +1,76 @@ +package v1 + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/entity" + "github.com/The-Gleb/go_metrics_and_alerting/internal/logger" + "github.com/The-Gleb/go_metrics_and_alerting/internal/repository" + "github.com/go-chi/chi/v5" +) + +const ( + getMetricJSONURL = "/value" +) + +type getMetricJSONHandler struct { + usecase GetMetricUsecase + middlewares []func(http.Handler) http.Handler +} + +func NewGetMetricJSONHandler(usecase GetMetricUsecase) *getMetricJSONHandler { + return &getMetricJSONHandler{ + usecase: usecase, + middlewares: make([]func(http.Handler) http.Handler, 0), + } +} + +func (h *getMetricJSONHandler) AddToRouter(r *chi.Mux) { + r.Route(getMetricJSONURL, func(r chi.Router) { + r.Use(h.middlewares...) + r.Post("/", h.ServeHTTP) + }) +} + +func (h *getMetricJSONHandler) Middlewares(md ...func(http.Handler) http.Handler) *getMetricJSONHandler { + h.middlewares = append(h.middlewares, md...) + return h +} + +func (h *getMetricJSONHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + + var metric entity.Metric + err := json.NewDecoder(r.Body).Decode(&metric) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + metric, err = h.usecase.GetMetric(r.Context(), metric) + if err != nil { + err = fmt.Errorf("handlers.GetMetricJSON: %w", err) + logger.Log.Error(err) + + if errors.Is(err, repository.ErrNotFound) { + rw.WriteHeader(http.StatusNotFound) + http.Error(rw, err.Error(), http.StatusNotFound) + return + } else { + rw.WriteHeader(http.StatusBadRequest) + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + } + + b, err := json.Marshal(metric) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + rw.Write(b) + +} diff --git a/internal/controller/http/v1/handler/get_metric_json_test.go b/internal/controller/http/v1/handler/get_metric_json_test.go new file mode 100644 index 0000000..13c7044 --- /dev/null +++ b/internal/controller/http/v1/handler/get_metric_json_test.go @@ -0,0 +1,66 @@ +package v1 + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/service" + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/usecase" + "github.com/The-Gleb/go_metrics_and_alerting/internal/repository/memory" + "github.com/go-chi/chi/v5" +) + +func Test_getMetricJSONHandler_ServeHTTP(t *testing.T) { + type args struct { + rw http.ResponseWriter + r *http.Request + } + tests := []struct { + name string + h *getMetricJSONHandler + args args + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.h.ServeHTTP(tt.args.rw, tt.args.r) + }) + } +} + +func Example_getMetricJSONHandler_ServeHTTP() { + + s := memory.New() + s.UpdateMetric("gauge", "Alloc", "3782369280") + metricService := service.NewMetricService(s) + getMetricUsecase := usecase.NewGetMetricUsecase(metricService) + getMetricJSONHandler := NewGetMetricJSONHandler(getMetricUsecase) + + router := chi.NewRouter() + getMetricJSONHandler.AddToRouter(router) + ts := httptest.NewServer(router) + defer ts.Close() + + validBody := `{ + "id": "Alloc", + "type": "gauge" + }` + + req, _ := http.NewRequest("POST", ts.URL+"/value", bytes.NewReader([]byte(validBody))) + + resp, _ := ts.Client().Do(req) + + fmt.Println(resp.StatusCode) + b, _ := io.ReadAll(resp.Body) + fmt.Println(string(b)) + + // Output: + // 200 + // {"id":"Alloc","type":"gauge","value":3782369280} + +} diff --git a/internal/controller/http/v1/handler/get_metric_test.go b/internal/controller/http/v1/handler/get_metric_test.go new file mode 100644 index 0000000..8404b41 --- /dev/null +++ b/internal/controller/http/v1/handler/get_metric_test.go @@ -0,0 +1,113 @@ +package v1 + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/service" + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/usecase" + "github.com/The-Gleb/go_metrics_and_alerting/internal/repository/memory" + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_getMetricHandler_ServeHTTP(t *testing.T) { + s := memory.New() + s.UpdateMetric("gauge", "Alloc", "123.4") + s.UpdateMetric("counter", "Counter", "123") + metricService := service.NewMetricService(s) + getMetricUsecase := usecase.NewGetMetricUsecase(metricService) + getMetricHandler := NewGetMetricHandler(getMetricUsecase) + + router := chi.NewRouter() + getMetricHandler.AddToRouter(router) + ts := httptest.NewServer(router) + defer ts.Close() + + type want struct { + value string + code int + } + tests := []struct { + name string + address string + want want + }{ + { + name: "normal gauge test #1", + address: "/value/gauge/Alloc", + want: want{ + value: "123.4", + code: 200, + }, + }, + { + name: "normal counter test #2", + address: "/value/counter/Counter", + want: want{ + value: "123", + code: 200, + }, + }, + { + name: "neg counter test #3", + address: "/value/counter/erff", + want: want{ + value: "", + code: 404, + }, + }, + { + name: "wrong metric type test #4", + address: "/value/ssds/erff", + want: want{ + value: "", + code: 400, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, val := testRequest(t, ts, "GET", tt.address, nil) + defer resp.Body.Close() + + require.Equal(t, tt.want.code, resp.StatusCode) + if tt.want.code != 200 { + return + } + assert.Equal(t, tt.want.value, string(val)) + + }) + } +} + +func Example_getMetricHandler_ServeHTTP() { + + s := memory.New() + s.UpdateMetric("gauge", "Alloc", "123.4") + metricService := service.NewMetricService(s) + getMetricUsecase := usecase.NewGetMetricUsecase(metricService) + getMetricHandler := NewGetMetricHandler(getMetricUsecase) + + router := chi.NewRouter() + getMetricHandler.AddToRouter(router) + ts := httptest.NewServer(router) + defer ts.Close() + + req, _ := http.NewRequest("GET", ts.URL+"/value/gauge/Alloc", nil) + + resp, _ := ts.Client().Do(req) + + fmt.Println(resp.StatusCode) + b, _ := io.ReadAll(resp.Body) + fmt.Println(string(b)) + + // Output: + // 200 + // 123.4 + +} diff --git a/internal/controller/http/v1/handler/update_metric.go b/internal/controller/http/v1/handler/update_metric.go new file mode 100644 index 0000000..c9e0ff9 --- /dev/null +++ b/internal/controller/http/v1/handler/update_metric.go @@ -0,0 +1,114 @@ +package v1 + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/entity" + "github.com/The-Gleb/go_metrics_and_alerting/internal/repository" + "github.com/go-chi/chi/v5" +) + +const ( + updateMetricURL = "/update/{type}/{name}/{value}" +) + +type UpdateMetricUsecase interface { + UpdateMetric(ctx context.Context, metrics entity.Metric) (entity.Metric, error) +} + +type updateMetricHandler struct { + usecase UpdateMetricUsecase + middlewares []func(http.Handler) http.Handler +} + +func NewUpdateMetricHandler(usecase UpdateMetricUsecase) *updateMetricHandler { + return &updateMetricHandler{ + usecase: usecase, + middlewares: make([]func(http.Handler) http.Handler, 0), + } +} + +func (h *updateMetricHandler) AddToRouter(r *chi.Mux) { + r.Route(updateMetricURL, func(r chi.Router) { + r.Use(h.middlewares...) + r.Post("/", h.ServeHTTP) + }) +} + +func (h *updateMetricHandler) Middlewares(md ...func(http.Handler) http.Handler) *updateMetricHandler { + h.middlewares = append(h.middlewares, md...) + return h +} + +func (h *updateMetricHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + + mType := chi.URLParam(r, "type") + mName := chi.URLParam(r, "name") + metric := entity.Metric{ + MType: mType, + ID: mName, + } + + switch mType { + case "gauge": + val, err := strconv.ParseFloat(chi.URLParam(r, "value"), 64) + if err != nil { + err = fmt.Errorf("getMetricHandler: %w", err) + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + metric.Value = &val + + metric, err = h.usecase.UpdateMetric(r.Context(), metric) + if err != nil { + if errors.Is(err, repository.ErrNotFound) { + http.Error(rw, err.Error(), http.StatusNotFound) + return + } else { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + } + + // strVal := strconv.FormatFloat(*metric.Value, 'g', -1, 64) + // rw.Write([]byte(strVal)) + + rw.Write([]byte(chi.URLParam(r, "value"))) + + case "counter": + delta, err := strconv.ParseInt(chi.URLParam(r, "value"), 10, 64) + if err != nil { + err = fmt.Errorf("getMetricHandler: %w", err) + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + metric.Delta = &delta + + metric, err = h.usecase.UpdateMetric(r.Context(), metric) + if err != nil { + if errors.Is(err, repository.ErrNotFound) { + rw.WriteHeader(http.StatusNotFound) + http.Error(rw, err.Error(), http.StatusNotFound) + return + } else { + rw.WriteHeader(http.StatusBadRequest) + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + } + + strVal := strconv.FormatInt(*metric.Delta, 10) + rw.Write([]byte(strVal)) + + // rw.Write([]byte(chi.URLParam(r, "value"))) + + default: + http.Error(rw, "invalid metric type", http.StatusBadRequest) + return + } + +} diff --git a/internal/controller/http/v1/handler/update_metric_json.go b/internal/controller/http/v1/handler/update_metric_json.go new file mode 100644 index 0000000..78388b4 --- /dev/null +++ b/internal/controller/http/v1/handler/update_metric_json.go @@ -0,0 +1,64 @@ +package v1 + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/entity" + "github.com/go-chi/chi/v5" +) + +const ( + updateMetricJSONURL = "/update" +) + +type updateMetricJSONHandler struct { + usecase UpdateMetricUsecase + middlewares []func(http.Handler) http.Handler +} + +func NewUpdateMetricJSONHandler(usecase UpdateMetricUsecase) *updateMetricJSONHandler { + return &updateMetricJSONHandler{ + usecase: usecase, + middlewares: make([]func(http.Handler) http.Handler, 0), + } +} + +func (h *updateMetricJSONHandler) AddToRouter(r *chi.Mux) { + r.Route(updateMetricJSONURL, func(r chi.Router) { + r.Use(h.middlewares...) + r.Post("/", h.ServeHTTP) + }) +} + +func (h *updateMetricJSONHandler) Middlewares(md ...func(http.Handler) http.Handler) *updateMetricJSONHandler { + h.middlewares = append(h.middlewares, md...) + return h +} + +func (h *updateMetricJSONHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + + var metric entity.Metric + err := json.NewDecoder(r.Body).Decode(&metric) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + metric, err = h.usecase.UpdateMetric(r.Context(), metric) + if err != nil { + err = fmt.Errorf("updateMetricJSONHandler: %w", err) + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + b, err := json.Marshal(metric) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + rw.Write(b) + +} diff --git a/internal/controller/http/v1/handler/update_metric_json_test.go b/internal/controller/http/v1/handler/update_metric_json_test.go new file mode 100644 index 0000000..efa30ad --- /dev/null +++ b/internal/controller/http/v1/handler/update_metric_json_test.go @@ -0,0 +1,46 @@ +package v1 + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/http/httptest" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/service" + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/usecase" + "github.com/The-Gleb/go_metrics_and_alerting/internal/repository/memory" + "github.com/go-chi/chi/v5" +) + +func Example_updateMetricJSONHandler_ServeHTTP() { + + s := memory.New() + metricService := service.NewMetricService(s) + updateMetricUsecase := usecase.NewUpdateMetricUsecase(metricService, nil) + updateMetricJSONHandler := NewUpdateMetricJSONHandler(updateMetricUsecase) + + router := chi.NewRouter() + updateMetricJSONHandler.AddToRouter(router) + ts := httptest.NewServer(router) + defer ts.Close() + + validBody := `{ + "id": "Alloc", + "type": "gauge", + "value": 123.123 + }` + + req, _ := http.NewRequest("POST", ts.URL+"/update", bytes.NewReader([]byte(validBody))) + + resp, _ := ts.Client().Do(req) + + fmt.Println(resp.StatusCode) + b, _ := io.ReadAll(resp.Body) + fmt.Println(string(b)) + + // Output: + // 200 + // {"id":"Alloc","type":"gauge","value":123.123} + +} diff --git a/internal/controller/http/v1/handler/update_metric_set.go b/internal/controller/http/v1/handler/update_metric_set.go new file mode 100644 index 0000000..fc7c58d --- /dev/null +++ b/internal/controller/http/v1/handler/update_metric_set.go @@ -0,0 +1,65 @@ +package v1 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/entity" + "github.com/go-chi/chi/v5" +) + +const ( + updateMetricSetURL = "/updates/" +) + +type UpdateMetricSetUsecase interface { + UpdateMetricSet(ctx context.Context, metrics []entity.Metric) (int64, error) +} + +type updateMetricSetHandler struct { + usecase UpdateMetricSetUsecase + middlewares []func(http.Handler) http.Handler +} + +func NewUpdateMetricSetHandler(usecase UpdateMetricSetUsecase) *updateMetricSetHandler { + return &updateMetricSetHandler{ + usecase: usecase, + middlewares: make([]func(http.Handler) http.Handler, 0), + } +} + +func (h *updateMetricSetHandler) AddToRouter(r *chi.Mux) { + r.Route(updateMetricSetURL, func(r chi.Router) { + r.Use(h.middlewares...) + r.Post("/", h.ServeHTTP) + }) +} + +func (h *updateMetricSetHandler) Middlewares(md ...func(http.Handler) http.Handler) *updateMetricSetHandler { + h.middlewares = append(h.middlewares, md...) + return h +} + +func (h *updateMetricSetHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + + var metrics []entity.Metric + err := json.NewDecoder(r.Body).Decode(&metrics) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + n, err := h.usecase.UpdateMetricSet(r.Context(), metrics) + if err != nil { + err = fmt.Errorf("updateMetricSetHandler: %w", err) + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + updatedMetricsCount := strconv.FormatInt(int64(n), 10) + rw.Write([]byte(updatedMetricsCount + " metrics updated")) + +} diff --git a/internal/controller/http/v1/handler/update_metric_set_test.go b/internal/controller/http/v1/handler/update_metric_set_test.go new file mode 100644 index 0000000..dbc61c9 --- /dev/null +++ b/internal/controller/http/v1/handler/update_metric_set_test.go @@ -0,0 +1,72 @@ +package v1 + +import ( + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/service" + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/usecase" + "github.com/The-Gleb/go_metrics_and_alerting/internal/repository/memory" + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/require" +) + +func Test_updateMetricSetHandler_ServeHTTP(t *testing.T) { + s := memory.New() + metricService := service.NewMetricService(s) + updateMetricSetUsecase := usecase.NewUpdateMetricSetUsecase(metricService, nil) + updateMetricSetHandler := NewUpdateMetricSetHandler(updateMetricSetUsecase) + + router := chi.NewRouter() + updateMetricSetHandler.AddToRouter(router) + ts := httptest.NewServer(router) + defer ts.Close() + + validJsonBody := `[ + { + "id": "HeapAlloc", + "type": "gauge", + "value": 3782369280 + }, + { + "id": "PollCount", + "type": "counter", + "delta": 123 + }]` + + type want struct { + code int + } + tests := []struct { + name string + body json.RawMessage + want want + }{ + { + name: "normal", + body: json.RawMessage(validJsonBody), + want: want{200}, + }, + { + name: "request with invalid body", + body: json.RawMessage([]byte("some invalid body")), + want: want{400}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + resp, b := testRequest(t, ts, "POST", "/updates/", tt.body) + defer resp.Body.Close() + + t.Log(b) + + require.Equal(t, tt.want.code, resp.StatusCode) + if tt.want.code != 200 { + return + } + }) + } +} diff --git a/internal/controller/http/v1/handler/update_metric_test.go b/internal/controller/http/v1/handler/update_metric_test.go new file mode 100644 index 0000000..6aa0784 --- /dev/null +++ b/internal/controller/http/v1/handler/update_metric_test.go @@ -0,0 +1,142 @@ +package v1 + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/service" + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/usecase" + "github.com/The-Gleb/go_metrics_and_alerting/internal/repository/memory" + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/require" +) + +func Test_updateMetricHandler_ServeHTTP(t *testing.T) { + s := memory.New() + metricService := service.NewMetricService(s) + updateMetricUsecase := usecase.NewUpdateMetricUsecase(metricService, nil) + updateMetricHandler := NewUpdateMetricHandler(updateMetricUsecase) + + router := chi.NewRouter() + updateMetricHandler.AddToRouter(router) + ts := httptest.NewServer(router) + defer ts.Close() + + type want struct { + code int + } + tests := []struct { + name string + address string + mType string + mName string + mValue string + want want + }{ + { + name: "normal gauge test #1", + address: "/update/gauge/Alloc/23.23", + mType: "gauge", + mName: "Alloc", + mValue: "23.23", + want: want{ + code: 200, + }, + }, + { + name: "first add counter test #2", + address: "/update/counter/counter/23", + mType: "counter", + mName: "counter", + mValue: "23", + want: want{ + code: 200, + }, + }, + { + name: "second add counter test #3", + address: "/update/counter/counter/7", + mType: "counter", + mName: "counter", + mValue: "30", + want: want{ + code: 200, + }, + }, + { + name: "name and value not sent - test #4", + address: "/update/gauge", + want: want{ + code: 404, + }, + }, + { + name: "value not sent - test #5", + address: "/update/gauge/nbhj", + want: want{ + code: 404, + }, + }, + { + name: "wrong metric type- test #5", + address: "/update/gaunjh/efvefv/eefe", + want: want{ + code: 400, + }, + }, + { + name: "incorrect metric value type - test #6", + address: "/update/gauge/alloc/string", + want: want{ + code: 400, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + resp, body := testRequest(t, ts, "POST", tt.address, nil) + defer resp.Body.Close() + + require.Equal(t, tt.want.code, resp.StatusCode) + if tt.want.code != 200 { + return + } + + require.Equal(t, tt.mValue, body) + + // val, _ := h.app.GetMetricFromParams(context.Background(), tt.mType, tt.mName) + // assert.Equal(t, tt.mValue, string(val)) + + }) + } +} + +func Example_updateMetricHandler_ServeHTTP() { + + s := memory.New() + metricService := service.NewMetricService(s) + updateMetricUsecase := usecase.NewUpdateMetricUsecase(metricService, nil) + updateMetricHandler := NewUpdateMetricHandler(updateMetricUsecase) + + router := chi.NewRouter() + updateMetricHandler.AddToRouter(router) + ts := httptest.NewServer(router) + defer ts.Close() + + req, _ := http.NewRequest("POST", ts.URL+"/update/gauge/Alloc/12.12", nil) + + resp, _ := ts.Client().Do(req) + + fmt.Println(resp.StatusCode) + b, _ := io.ReadAll(resp.Body) + fmt.Println(string(b)) + + // Output: + // 200 + // 12.12 + +} diff --git a/internal/controller/http/v1/middleware/check_signature.go b/internal/controller/http/v1/middleware/check_signature.go new file mode 100644 index 0000000..096a2c9 --- /dev/null +++ b/internal/controller/http/v1/middleware/check_signature.go @@ -0,0 +1,93 @@ +package middleware + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "io" + "net/http" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/logger" +) + +type checkSignatureMiddleware struct { + signKey []byte +} + +func NewCheckSignatureMiddleware(signKey []byte) *checkSignatureMiddleware { + return &checkSignatureMiddleware{signKey: signKey} +} + +type signingResponseWriter struct { + http.ResponseWriter + key []byte +} + +func (w *signingResponseWriter) Write(b []byte) (int, error) { + h := hmac.New(sha256.New, w.key) + _, err := h.Write(b) + if err != nil { + http.Error(w.ResponseWriter, err.Error(), http.StatusInternalServerError) + } + sign := h.Sum(nil) + encodedSign := hex.EncodeToString(sign) + w.ResponseWriter.Header().Set("HashSHA256", encodedSign) + n, err := w.ResponseWriter.Write(b) + return n, err +} + +func (md *checkSignatureMiddleware) Do(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + if len(md.signKey) == 0 || r.Header.Get("Hash") == "none" || r.Header.Get("Hashsha256") == "" { + logger.Log.Debug("key is empty") + next.ServeHTTP(w, r) + return + } + + if r.Header.Get("Hash") == "none" { + logger.Log.Debug("key is empty") + next.ServeHTTP(w, r) + return + } + + gotSign, err := hex.DecodeString(r.Header.Get("HashSHA256")) + // gotSign := r.Header.Get("HashSHA256") + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + h := hmac.New(sha256.New, md.signKey) + + data, _ := io.ReadAll(r.Body) + r.Body.Close() + _, err = h.Write(data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + sign := h.Sum(nil) + + logger.Log.Debug("sign key is ", string(md.signKey)) + logger.Log.Debug("received body is ", string(data)) + logger.Log.Debug("received hex signature: ", r.Header.Get("HashSHA256")) + logger.Log.Debug("calculated hex signature: ", hex.EncodeToString(sign)) + logger.Log.Debug("Headers: ", r.Header) + + if !hmac.Equal(sign, []byte(gotSign)) { + logger.Log.Debug("hash signatures are not equal") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + return + } + + r.Body = io.NopCloser(bytes.NewBuffer(data)) + srw := signingResponseWriter{ + ResponseWriter: w, + key: md.signKey, + } + next.ServeHTTP(&srw, r) + + } + return http.HandlerFunc(fn) +} diff --git a/internal/controller/http/v1/middleware/compressor.go b/internal/controller/http/v1/middleware/compressor.go new file mode 100644 index 0000000..e6fd87a --- /dev/null +++ b/internal/controller/http/v1/middleware/compressor.go @@ -0,0 +1,114 @@ +package middleware + +import ( + "compress/gzip" + "fmt" + "io" + + // "log" + "net/http" + "strings" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/logger" +) + +type gzipMiddleware struct { +} + +func NewGzipMiddleware() *gzipMiddleware { + return &gzipMiddleware{} +} + +type compressWriter struct { + rw http.ResponseWriter + zw *gzip.Writer +} + +func newCompressWriter(rw http.ResponseWriter) *compressWriter { + return &compressWriter{ + rw: rw, + zw: gzip.NewWriter(rw), + } +} + +func (c *compressWriter) Header() http.Header { + return c.rw.Header() +} + +func (c *compressWriter) Write(p []byte) (int, error) { + if c.rw.Header().Get("Content-Type") == "application/json" || c.rw.Header().Get("Content-Type") == "text/html" { + c.rw.Header().Set("Content-Encoding", "gzip") + return c.zw.Write(p) + } + return c.rw.Write(p) +} +func (c *compressWriter) WriteHeader(statusCode int) { + if statusCode < 300 { + c.rw.Header().Set("Content-Encoding", "gzip") + } + c.rw.WriteHeader(statusCode) +} + +func (c *compressWriter) Close() error { + return c.zw.Close() +} + +type compressReader struct { + r io.ReadCloser + zr *gzip.Reader +} + +func newCompressReader(r io.ReadCloser) (*compressReader, error) { + zr, err := gzip.NewReader(r) + if err != nil { + return nil, fmt.Errorf("gzip.NewReader: %w", err) + } + + return &compressReader{ + r: r, + zr: zr, + }, nil +} + +func (c compressReader) Read(p []byte) (n int, err error) { + return c.zr.Read(p) +} + +func (c *compressReader) Close() error { + if err := c.r.Close(); err != nil { + return err + } + return c.zr.Close() +} + +func (md *gzipMiddleware) Do(h http.Handler) http.Handler { + gzipMiddleware := func(rw http.ResponseWriter, r *http.Request) { + ow := rw + + acceptEncoding := r.Header.Get("Accept-Encoding") + supportsGzip := strings.Contains(acceptEncoding, "gzip") + // logger.Log.Debugw("Request Accept-Encoding", "gzip", supportsGzip) + if supportsGzip { + cw := newCompressWriter(rw) + ow = cw + defer cw.Close() + } + + contentEncoding := r.Header.Get("Content-Encoding") + sendsGzip := strings.Contains(contentEncoding, "gzip") + // logger.Log.Debugw("Request Content-Encoding", "gzip", sendsGzip) + if sendsGzip { + cr, err := newCompressReader(r.Body) + if err != nil { + logger.Log.Error(err) + rw.WriteHeader(http.StatusInternalServerError) + return + } + r.Body = cr + defer cr.Close() + } + + h.ServeHTTP(ow, r) + } + return http.HandlerFunc(gzipMiddleware) +} diff --git a/internal/controller/http/v1/middleware/logger.go b/internal/controller/http/v1/middleware/logger.go new file mode 100644 index 0000000..24c598c --- /dev/null +++ b/internal/controller/http/v1/middleware/logger.go @@ -0,0 +1,72 @@ +package middleware + +import ( + "net/http" + "time" + + "go.uber.org/zap" +) + +type loggerMiddleware struct { + log *zap.SugaredLogger +} + +func NewLoggerMiddleware(l *zap.SugaredLogger) *loggerMiddleware { + return &loggerMiddleware{l} +} + +type ( + responseData struct { + status int + size int + } + + loggingResponseWriter struct { + http.ResponseWriter + responseData *responseData + } +) + +func (r *loggingResponseWriter) Write(b []byte) (int, error) { + size, err := r.ResponseWriter.Write(b) + r.responseData.status = 200 + r.responseData.size += size + return size, err +} + +func (r *loggingResponseWriter) WriteHeader(statusCode int) { + r.ResponseWriter.WriteHeader(statusCode) + r.responseData.status = statusCode +} + +func (md *loggerMiddleware) Do(next http.Handler) http.Handler { + logFn := func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + responseData := &responseData{ + status: 0, + size: 0, + } + lw := loggingResponseWriter{ + ResponseWriter: w, + responseData: responseData, + } + + next.ServeHTTP(&lw, r) + + buf := make([]byte, 0) + r.Body.Read(buf) + duration := time.Since(start) + + md.log.Infow( + "Got request ", + "method", r.Method, + "uri", r.RequestURI, + // "request body", string(buf), + "status", responseData.status, + "duration", duration, + "size", responseData.size, + ) + } + return http.HandlerFunc(logFn) +} diff --git a/internal/domain/entity/metric.go b/internal/domain/entity/metric.go new file mode 100644 index 0000000..fde238a --- /dev/null +++ b/internal/domain/entity/metric.go @@ -0,0 +1,12 @@ +package entity + +type Metric struct { + ID string `json:"id"` // имя метрики + MType string `json:"type"` // параметр, принимающий значение gauge или counter + Delta *int64 `json:"delta,omitempty"` // значение метрики в случае передачи counter + Value *float64 `json:"value,omitempty"` // значение метрики в случае передачи gauge +} +type MetricsMaps struct { + Gauge []Metric + Counter []Metric +} diff --git a/internal/domain/service/backup.go b/internal/domain/service/backup.go new file mode 100644 index 0000000..7acc72f --- /dev/null +++ b/internal/domain/service/backup.go @@ -0,0 +1,80 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/entity" +) + +type FileStorage interface { + WriteData(data []byte) error + ReadData() ([]byte, error) +} + +type backupService struct { + metricStorage MetricStorage + backupStorage FileStorage + backupInterval int + restore bool +} + +func NewBackupService(ms MetricStorage, bs FileStorage, interval int, restore bool) *backupService { + return &backupService{ + metricStorage: ms, + backupStorage: bs, + backupInterval: interval, + restore: restore, + } +} + +func (service *backupService) LoadDataFromFile(ctx context.Context) error { + + data, err := service.backupStorage.ReadData() + if err != nil { + return fmt.Errorf("LoadDataFromFile: failed reading data: %w", err) + } + + var maps entity.MetricsMaps + + err = json.Unmarshal(data, &maps) + if err != nil { + return fmt.Errorf("LoadDataFromFile: failed unmarshalling: %w", err) + } + + _, err = service.metricStorage.UpdateMetricSet(ctx, maps.Gauge) + if err != nil { + return fmt.Errorf("LoadDataFromFile: failded updating gauge: %w", err) + } + + _, err = service.metricStorage.UpdateMetricSet(ctx, maps.Counter) + if err != nil { + return fmt.Errorf("LoadDataFromFile: failded updating counter: %w", err) + } + + return nil +} + +func (service *backupService) StoreDataToFile(ctx context.Context) error { + metricMaps, err := service.metricStorage.GetAllMetrics(ctx) + if err != nil { + return fmt.Errorf("StoreDataToFile: %w", err) + } + + data, err := json.Marshal(metricMaps) + if err != nil { + return fmt.Errorf("StoreDataToFile: %w", err) + } + + err = service.backupStorage.WriteData(data) + if err != nil { + return fmt.Errorf("StoreDataToFile: %w", err) + } + + return nil +} + +func (service *backupService) SyncWrite() bool { + return service.backupInterval == 0 +} diff --git a/internal/domain/service/metric.go b/internal/domain/service/metric.go new file mode 100644 index 0000000..4d17515 --- /dev/null +++ b/internal/domain/service/metric.go @@ -0,0 +1,85 @@ +package service + +import ( + "context" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/entity" +) + +type MetricStorage interface { + UpdateGauge(ctx context.Context, metric entity.Metric) (entity.Metric, error) + UpdateCounter(ctx context.Context, metric entity.Metric) (entity.Metric, error) + + UpdateMetricSet(ctx context.Context, metrics []entity.Metric) (int64, error) + + GetGauge(ctx context.Context, metric entity.Metric) (entity.Metric, error) + GetCounter(ctx context.Context, metric entity.Metric) (entity.Metric, error) + GetAllMetrics(ctx context.Context) (entity.MetricsMaps, error) + + PingDB() error +} + +type metricService struct { + storage MetricStorage +} + +func NewMetricService(s MetricStorage) *metricService { + return &metricService{s} +} + +func (service *metricService) UpdateMetric(ctx context.Context, metric entity.Metric) (entity.Metric, error) { + + var err error + switch metric.MType { + case "gauge": + metric, err = service.storage.UpdateGauge(ctx, metric) + + case "counter": + metric, err = service.storage.UpdateCounter(ctx, metric) + } + + if err != nil { + return entity.Metric{}, err + } + + return metric, nil + +} + +func (service *metricService) UpdateMetricSet(ctx context.Context, metrics []entity.Metric) (int64, error) { + + return service.storage.UpdateMetricSet(ctx, metrics) + +} + +func (service *metricService) GetMetric(ctx context.Context, metric entity.Metric) (entity.Metric, error) { + + var err error + switch metric.MType { + case "gauge": + metric, err = service.storage.GetGauge(ctx, metric) + + case "counter": + metric, err = service.storage.GetCounter(ctx, metric) + } + + if err != nil { + return entity.Metric{}, err + } + + return metric, nil + +} + +func (service *metricService) GetAllMetrics(ctx context.Context) (entity.MetricsMaps, error) { + + return service.storage.GetAllMetrics(ctx) + +} + +// why does metric service ping database??? +func (service *metricService) PingDB() error { + + return service.storage.PingDB() + +} diff --git a/internal/domain/usecase/get_all_metrics.go b/internal/domain/usecase/get_all_metrics.go new file mode 100644 index 0000000..3563f3a --- /dev/null +++ b/internal/domain/usecase/get_all_metrics.go @@ -0,0 +1,89 @@ +package usecase + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + + "github.com/The-Gleb/go_metrics_and_alerting/internal/domain/entity" + "github.com/The-Gleb/go_metrics_and_alerting/pkg/utils/retry" +) + +type getAllMetricsUsecase struct { + metricService MetricService +} + +func NewGetAllMetricsUsecase(ms MetricService) *getAllMetricsUsecase { + return &getAllMetricsUsecase{ + metricService: ms, + } +} + +func (uc *getAllMetricsUsecase) GetAllMetricsJSON(ctx context.Context) ([]byte, error) { + + var metricMaps entity.MetricsMaps + var err error + err = retry.DefaultRetry(context.TODO(), func(ctx context.Context) error { + metricMaps, err = uc.metricService.GetAllMetrics(ctx) + return err + }) + if err != nil { + return []byte{}, fmt.Errorf("GetAllMetricsJSON: %w", err) + } + + b := new(bytes.Buffer) + + jsonMaps, err := json.Marshal(&metricMaps) + if err != nil { + return []byte{}, fmt.Errorf("GetAllMetricsJSON: %w", err) + } + + _, err = b.Write(jsonMaps) + if err != nil { + return []byte{}, fmt.Errorf("GetAllMetricsJSON: %w", err) + } + + return b.Bytes(), nil + +} + +func (uc *getAllMetricsUsecase) GetAllMetricsHTML(ctx context.Context) ([]byte, error) { + + var metricMaps entity.MetricsMaps + var err error + err = retry.DefaultRetry(ctx, func(ctx context.Context) error { + metricMaps, err = uc.metricService.GetAllMetrics(ctx) + return err + }) + if err != nil { + return []byte{}, fmt.Errorf("GetAllMetricsHTML: %w", err) + } + + b := new(bytes.Buffer) + fmt.Fprintf(b, ` + +
+ +