From 07dde0f6ff7c3396abcd344bffcf33bbed9788e8 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Thu, 6 Feb 2025 00:36:56 +0300 Subject: [PATCH 1/9] compose fix kafka & add minio --- deploy/prometheus.yml | 2 +- docker-compose.yaml | 74 +++++++++++++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/deploy/prometheus.yml b/deploy/prometheus.yml index 0d296d7..56d26b2 100644 --- a/deploy/prometheus.yml +++ b/deploy/prometheus.yml @@ -34,6 +34,6 @@ scrape_configs: - job_name: 'machine' scrape_interval: 2s static_configs: - - targets: ['host.docker.internal:9100'] + - targets: ['node_exporter:9100'] labels: group: 'backend' \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index cfbd7ea..0262ab0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -56,15 +56,20 @@ services: node_exporter: image: quay.io/prometheus/node-exporter:latest + pid: host command: - - '--path.rootfs=/host' - ports: - - 9100:9100 + - '--path.procfs=/host/proc' + - '--path.rootfs=/rootfs' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro extra_hosts: - "host.docker.internal:host-gateway" - pid: host - volumes: - - '/:/host:ro,rslave' + ports: + - 9100:9100 otel-collector: image: otel/opentelemetry-collector-contrib:0.108.0 @@ -97,12 +102,17 @@ services: - tempo-init kafka: - image: apache/kafka:3.8.0 + image: &kafkaImage apache/kafka:3.8.0 + healthcheck: + test: ["CMD-SHELL", "/opt/kafka/bin/kafka-cluster.sh cluster-id --bootstrap-server http://127.0.0.1:9092 || exit 1"] + interval: 1s + timeout: 30s + retries: 30 environment: KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093 @@ -113,18 +123,48 @@ services: KAFKA_NUM_PARTITIONS: 3 ports: - 9092:9092 - # - 9093:9093 - # backend: - # build: . - # # dockerfile: ./dockerfile - # volumes: - # - ./:/app - # ports: - # - 8080:8080 + kafka-init: + image: *kafkaImage + depends_on: + kafka: + condition: service_healthy + entrypoint: > + /bin/bash -c "/opt/kafka/bin/kafka-topics.sh --bootstrap-server http://kafka:9092 --create --topic events --partitions 6" + + + minio: + image: quay.io/minio/minio:latest + command: ["server", "/data", "--console-address", ":9001"] + healthcheck: + test: 'mc ready local' + interval: 1s + environment: + MINIO_ROOT_USER: miniouser + MINIO_ROOT_PASSWORD: miniouser + MINIO_ACCESS_KEY: miniokey + MINIO_SECRET_KEY: miniokey + ports: + - 9000:9000 + - 9001:9001 + volumes: + - minio-volume:/data + + minio-init: + image: quay.io/minio/mc:latest + depends_on: + - minio + entrypoint: > + /bin/sh -c " + /usr/bin/mc alias set myminio http://minio:9000 miniouser miniouser; + /usr/bin/mc mb minio/bucket; + /usr/bin/mc anonymous set public minio/bucket; + exit 0; + " volumes: postgres-volume: grafana-volume: tempo-volume: - prometheus-volume: \ No newline at end of file + prometheus-volume: + minio-volume: \ No newline at end of file From cf01b1e36f914776897f58fc1ddc38c3b0ef4118 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Fri, 7 Feb 2025 12:09:23 +0300 Subject: [PATCH 2/9] add wrapper for server handlers --- cmd/backend/config_defaults/config.yaml | 2 +- .../server/handlers/user_create_handler.go | 72 +++++----------- .../server/handlers/user_login_handler.go | 53 ++++-------- cmd/shortlinks/grpc.go | 31 +++++++ cmd/shortlinks/handlers.go | 83 +++++-------------- cmd/shortlinks/main.go | 11 +-- internal/http_server/wrapper.go | 79 ++++++++++++++++++ 7 files changed, 173 insertions(+), 158 deletions(-) create mode 100644 cmd/shortlinks/grpc.go create mode 100644 internal/http_server/wrapper.go diff --git a/cmd/backend/config_defaults/config.yaml b/cmd/backend/config_defaults/config.yaml index d5df5f4..f2e060c 100644 --- a/cmd/backend/config_defaults/config.yaml +++ b/cmd/backend/config_defaults/config.yaml @@ -1,5 +1,5 @@ port: 8080 postgres_url: "postgres://postgres:postgres@localhost:5432/postgres" -jwt_signing_key: "./jwt_signing_key" +jwt_signing_key: "./config_defaults/jwt_signing_key" kafka_url: "localhost:9092" kafka_topic: "backend_events" \ No newline at end of file diff --git a/cmd/backend/server/handlers/user_create_handler.go b/cmd/backend/server/handlers/user_create_handler.go index 09610e0..70d4a30 100644 --- a/cmd/backend/server/handlers/user_create_handler.go +++ b/cmd/backend/server/handlers/user_create_handler.go @@ -2,8 +2,9 @@ package handlers import ( "backend/internal/core/services" + httpserver "backend/internal/http_server" "backend/pkg/logger" - "encoding/json" + "context" "github.com/gin-gonic/gin" ) @@ -20,54 +21,27 @@ type createUserOutput struct { Name string `json:"name"` } -func NewUserCreateHandler(logger logger.Logger, userService services.UserService) gin.HandlerFunc { - return func(c *gin.Context) { - ctxLogger := logger.WithContext(c) +func NewUserCreateHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { + return httpserver.WrapGin(log, + func(ctx context.Context, input createUserInput) (createUserOutput, error) { + user, err := userService.CreateUser(ctx, + services.UserCreateParams{ + Email: input.Email, + Password: input.Password, + Name: input.Name, + }, + ) - params := createUserInput{} - if err := c.ShouldBindJSON(¶ms); err != nil { - ctxLogger.Error().Err(err).Msg("bad input body model") - c.Data(400, "plain/text", []byte(err.Error())) - return - } + out := createUserOutput{} + if err != nil { + return out, err + } - dto, err := userService.CreateUser( - c, - services.UserCreateParams{ - Email: params.Email, - Password: params.Password, - Name: params.Name, - }, - ) - if err == services.ErrUserExists { - ctxLogger.Error().Err(err).Msg("user already exists") - c.Data(400, "plain/text", []byte(err.Error())) - return - } - if err == services.ErrUserBadPassword { - ctxLogger.Error().Err(err).Msg("password does not satisfy requirements") - c.Data(400, "plain/text", []byte(err.Error())) - return - } - if err != nil { - ctxLogger.Error().Err(err).Msg("unexpected create user error") - c.Data(500, "plain/text", []byte(err.Error())) - return - } - - resultBody, err := json.Marshal( - createUserOutput{ - Id: dto.Id, - Email: dto.Email, - Name: dto.Name, - }, - ) - if err != nil { - ctxLogger.Error().Err(err).Msg("marshal user model error") - c.Data(500, "plain/text", []byte(err.Error())) - return - } - - c.Data(200, "application/json", resultBody) - } + return createUserOutput{ + Id: user.Id, + Email: user.Email, + Name: user.Name, + }, nil + }, + ) } diff --git a/cmd/backend/server/handlers/user_login_handler.go b/cmd/backend/server/handlers/user_login_handler.go index d1878d4..72aa789 100644 --- a/cmd/backend/server/handlers/user_login_handler.go +++ b/cmd/backend/server/handlers/user_login_handler.go @@ -2,8 +2,9 @@ package handlers import ( "backend/internal/core/services" + httpserver "backend/internal/http_server" "backend/pkg/logger" - "encoding/json" + "context" "github.com/gin-gonic/gin" ) @@ -17,43 +18,17 @@ type loginUserOutput struct { Token string `json:"token"` } -func NewUserLoginHandler(logger logger.Logger, userService services.UserService) gin.HandlerFunc { - return func(c *gin.Context) { - ctxLogger := logger.WithContext(c).WithPrefix("NewUserLoginHandler") +func NewUserLoginHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { + return httpserver.WrapGin(log, + func(ctx context.Context, input loginUserInput) (loginUserOutput, error) { + token, err := userService.AuthenticateUser(ctx, input.Login, input.Password) + if err != nil { + return loginUserOutput{}, err + } - params := loginUserInput{} - if err := c.ShouldBindJSON(¶ms); err != nil { - ctxLogger.Error().Err(err).Msg("bad input body model") - c.AbortWithError(400, err) - return - } - - token, err := userService.AuthenticateUser(c, params.Login, params.Password) - if err == services.ErrUserNotExists { - ctxLogger.Error().Err(err).Msg("user does not exist") - c.AbortWithError(400, err) - return - } - if err == services.ErrUserWrongPassword { - ctxLogger.Error().Err(err).Msg("wrong password") - c.AbortWithError(400, err) - return - } - if err != nil { - ctxLogger.Error().Err(err).Msg("AuthenticateUser internal error") - c.AbortWithError(500, err) - return - } - - resultBody, err := json.Marshal(loginUserOutput{ - Token: token, - }) - if err != nil { - ctxLogger.Error().Err(err).Msg("marshal json internal error") - c.AbortWithError(500, err) - return - } - - c.Data(200, "application/json", resultBody) - } + return loginUserOutput{ + Token: token, + }, nil + }, + ) } diff --git a/cmd/shortlinks/grpc.go b/cmd/shortlinks/grpc.go new file mode 100644 index 0000000..c35bf71 --- /dev/null +++ b/cmd/shortlinks/grpc.go @@ -0,0 +1,31 @@ +package main + +import ( + "backend/internal/core/services" + "backend/internal/grpc_server/shortlinks" + httpserver "backend/internal/http_server" + "backend/pkg/logger" + "context" +) + +func NewShortlinksGrpc(log logger.Logger, shortlinkService services.ShortlinkService, host string) *ShortlinksGrpc { + return &ShortlinksGrpc{ + handler: NewCreateHandler(log, shortlinkService, host), + } +} + +type ShortlinksGrpc struct { + shortlinks.UnimplementedShortlinksServer + handler httpserver.Handler[shortlinkCreateInput, shortlinkCreateOutput] +} + +func (s *ShortlinksGrpc) Create(ctx context.Context, req *shortlinks.CreateRequest) (*shortlinks.CreateResponse, error) { + output, err := s.handler(ctx, shortlinkCreateInput{req.Url}) + if err != nil { + return nil, err + } + + return &shortlinks.CreateResponse{ + Link: output.Link, + }, nil +} diff --git a/cmd/shortlinks/handlers.go b/cmd/shortlinks/handlers.go index 3fdc089..0af19af 100644 --- a/cmd/shortlinks/handlers.go +++ b/cmd/shortlinks/handlers.go @@ -2,93 +2,52 @@ package main import ( "backend/internal/core/services" - "backend/internal/grpc_server/shortlinks" + httpserver "backend/internal/http_server" "backend/pkg/logger" "context" - "encoding/json" "fmt" "net/url" "github.com/gin-gonic/gin" ) +type shortlinkCreateInput struct { + Url string `json:"url"` +} + type shortlinkCreateOutput struct { Link string `json:"link"` } -type ShortlinksGrpc struct { - shortlinks.UnimplementedShortlinksServer - log logger.Logger - host string - shortlinkService services.ShortlinkService -} +func NewCreateHandler( + log logger.Logger, + shortlinkService services.ShortlinkService, + host string, +) httpserver.Handler[shortlinkCreateInput, shortlinkCreateOutput] { + return func(ctx context.Context, input shortlinkCreateInput) (shortlinkCreateOutput, error) { + output := shortlinkCreateOutput{} -func (s *ShortlinksGrpc) Create(ctx context.Context, req *shortlinks.CreateRequest) (*shortlinks.CreateResponse, error) { - ctxLogger := s.log.WithContext(ctx) - - rawUrl := req.GetUrl() - if rawUrl == "" { - ctxLogger.Error().Msg("url query param missing") - return nil, fmt.Errorf("url query param missing") - } - - u, err := url.Parse(rawUrl) - if err != nil { - ctxLogger.Error().Err(err).Msg("error parsing url param") - return nil, err - } - u.Scheme = "https" - - linkId, err := s.shortlinkService.CreateShortlink(ctx, u.String()) - if err != nil { - ctxLogger.Error().Err(err).Msg("err creating shortlink") - return nil, err - } - - return &shortlinks.CreateResponse{ - Link: fmt.Sprintf("%s/s/%s", s.host, linkId), - }, nil -} - -func NewShortlinkCreateHandler(logger logger.Logger, shortlinkService services.ShortlinkService, host string) gin.HandlerFunc { - return func(ctx *gin.Context) { - ctxLogger := logger.WithContext(ctx) - - rawUrl := ctx.Query("url") - if rawUrl == "" { - ctxLogger.Error().Msg("url query param missing") - ctx.AbortWithError(400, fmt.Errorf("url query param missing")) - return - } - - u, err := url.Parse(rawUrl) + u, err := url.Parse(input.Url) if err != nil { - ctxLogger.Error().Err(err).Msg("error parsing url param") - ctx.Data(400, "plain/text", []byte(err.Error())) - return + return output, err } u.Scheme = "https" linkId, err := shortlinkService.CreateShortlink(ctx, u.String()) if err != nil { - ctxLogger.Error().Err(err).Msg("err creating shortlink") - ctx.Data(500, "plain/text", []byte(err.Error())) - return + return output, err } - resultBody, err := json.Marshal(shortlinkCreateOutput{ + return shortlinkCreateOutput{ Link: fmt.Sprintf("%s/s/%s", host, linkId), - }) - if err != nil { - ctxLogger.Error().Err(err).Msg("err marshalling shortlink") - ctx.AbortWithError(500, err) - return - } - - ctx.Data(200, "application/json", resultBody) + }, nil } } +func NewShortlinkCreateHandler(log logger.Logger, shortlinkService services.ShortlinkService, host string) gin.HandlerFunc { + return httpserver.WrapGin(log, NewCreateHandler(log, shortlinkService, host)) +} + func NewShortlinkResolveHandler(logger logger.Logger, shortlinkService services.ShortlinkService) gin.HandlerFunc { return func(ctx *gin.Context) { ctxLogger := logger.WithContext(ctx) diff --git a/cmd/shortlinks/main.go b/cmd/shortlinks/main.go index d6ef4ed..4fb9324 100644 --- a/cmd/shortlinks/main.go +++ b/cmd/shortlinks/main.go @@ -102,14 +102,11 @@ func RunServer(ctx context.Context, log logger.Logger, tracer trace.Tracer, conf linkGroup.POST("/new", NewShortlinkCreateHandler(log, shortlinkService, host)) linkGroup.GET("/:linkId", NewShortlinkResolveHandler(log, shortlinkService)) - grpcObj := &ShortlinksGrpc{ - log: log, - host: host, - shortlinkService: shortlinkService, - } - grpcUnderlying := grpc.NewServer() - shortlinks.RegisterShortlinksServer(grpcUnderlying, grpcObj) + shortlinks.RegisterShortlinksServer( + grpcUnderlying, + NewShortlinksGrpc(log, shortlinkService, host), + ) httpServer := httpserver.New( httpserver.NewServerOpts{ diff --git a/internal/http_server/wrapper.go b/internal/http_server/wrapper.go new file mode 100644 index 0000000..0f866ee --- /dev/null +++ b/internal/http_server/wrapper.go @@ -0,0 +1,79 @@ +package httpserver + +import ( + "backend/pkg/logger" + "context" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +type Handler[Input, Output any] func(ctx context.Context, input Input) (Output, error) + +type ResponseOk struct { + Status string `json:"status"` + Result interface{} `json:"result"` +} + +type ResponseError struct { + Status string `json:"status"` + Error struct { + Id string `json:"id"` + Message string `json:"message"` + } `json:"error"` +} + +func WrapGin[In, Out interface{}](log logger.Logger, handler Handler[In, Out]) gin.HandlerFunc { + return func(c *gin.Context) { + log := log.WithContext(c) + + var input In + if err := c.ShouldBindJSON(&input); err != nil { + response := ResponseError{ + Status: "error", + Error: struct { + Id string `json:"id"` + Message string `json:"message"` + }{ + Id: "WrongBody", + Message: err.Error(), + }, + } + + body, _ := json.Marshal(response) + c.Data(400, "application/json", body) + return + } + + var response interface{} + + output, err := handler(c, input) + if err != nil { + log.Error().Err(err).Msg("error in request handler") + response = ResponseError{ + Status: "error", + Error: struct { + Id string `json:"id"` + Message string `json:"message"` + }{ + Id: "-", + Message: err.Error(), + }, + } + } else { + response = ResponseOk{ + Status: "success", + Result: output, + } + } + + body, err := json.Marshal(response) + if err != nil { + log.Error().Err(err).Msg("marshal response error") + c.Data(500, "plain/text", []byte(err.Error())) + return + } + + c.Data(200, "application/json", body) + } +} From 5baf5ed7e9686d9da63dfcf0bea90a64fb6ea281 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Fri, 7 Feb 2025 12:03:46 +0300 Subject: [PATCH 3/9] remove kafka topic autocreation --- internal/integrations/kafka.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/integrations/kafka.go b/internal/integrations/kafka.go index ebd0b7c..69eef72 100644 --- a/internal/integrations/kafka.go +++ b/internal/integrations/kafka.go @@ -16,7 +16,7 @@ func NewKafka(addr, topic string) *Kafka { Addr: kafka.TCP(addr), Topic: topic, Balancer: &kafka.RoundRobin{}, - AllowAutoTopicCreation: true, + AllowAutoTopicCreation: false, BatchSize: 100, BatchTimeout: 100 * time.Millisecond, } From 8ddfe7a6266289f7d31a034cc1317b8ef174c0c0 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Fri, 7 Feb 2025 13:07:19 +0300 Subject: [PATCH 4/9] validate email fields in request --- cmd/backend/server/handlers/user_create_handler.go | 6 +++--- cmd/backend/server/handlers/user_login_handler.go | 2 +- internal/http_server/wrapper.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/backend/server/handlers/user_create_handler.go b/cmd/backend/server/handlers/user_create_handler.go index 70d4a30..392bbdb 100644 --- a/cmd/backend/server/handlers/user_create_handler.go +++ b/cmd/backend/server/handlers/user_create_handler.go @@ -10,9 +10,9 @@ import ( ) type createUserInput struct { - Email string `json:"email"` - Password string `json:"password"` - Name string `json:"name"` + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required"` + Name string `json:"name" validate:"required"` } type createUserOutput struct { diff --git a/cmd/backend/server/handlers/user_login_handler.go b/cmd/backend/server/handlers/user_login_handler.go index 72aa789..90193dd 100644 --- a/cmd/backend/server/handlers/user_login_handler.go +++ b/cmd/backend/server/handlers/user_login_handler.go @@ -10,7 +10,7 @@ import ( ) type loginUserInput struct { - Login string `json:"email"` + Login string `json:"email" validate:"required,email"` Password string `json:"password"` } diff --git a/internal/http_server/wrapper.go b/internal/http_server/wrapper.go index 0f866ee..f96d6a4 100644 --- a/internal/http_server/wrapper.go +++ b/internal/http_server/wrapper.go @@ -8,7 +8,7 @@ import ( "github.com/gin-gonic/gin" ) -type Handler[Input, Output any] func(ctx context.Context, input Input) (Output, error) +type Handler[Input, Output interface{}] func(ctx context.Context, input Input) (Output, error) type ResponseOk struct { Status string `json:"status"` From 1608ad15075c91cafa26c98625d6ff565d48590a Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Fri, 7 Feb 2025 13:26:22 +0300 Subject: [PATCH 5/9] move server functions to internal/httpserver --- cmd/backend/app.go | 2 +- cmd/backend/server/middleware/request_log.go | 56 ------- cmd/backend/server/server.go | 48 ++---- cmd/shortlinks/main.go | 7 +- internal/http_server/middleware/recovery.go | 155 ------------------ internal/http_server/middleware/tracing.go | 33 ---- .../http_server}/recovery.go | 2 +- .../{middleware => }/request_log.go | 2 +- .../http_server}/tracing.go | 2 +- 9 files changed, 18 insertions(+), 289 deletions(-) delete mode 100644 cmd/backend/server/middleware/request_log.go delete mode 100644 internal/http_server/middleware/recovery.go delete mode 100644 internal/http_server/middleware/tracing.go rename {cmd/backend/server/middleware => internal/http_server}/recovery.go (99%) rename internal/http_server/{middleware => }/request_log.go (98%) rename {cmd/backend/server/middleware => internal/http_server}/tracing.go (97%) diff --git a/cmd/backend/app.go b/cmd/backend/app.go index aa520d2..59c1b26 100644 --- a/cmd/backend/app.go +++ b/cmd/backend/app.go @@ -186,7 +186,7 @@ func (a *App) Run(p RunParams) { }() } - srv := server.New( + srv := server.NewServer( server.NewServerOpts{ DebugMode: debugMode, Logger: logger, diff --git a/cmd/backend/server/middleware/request_log.go b/cmd/backend/server/middleware/request_log.go deleted file mode 100644 index 587abc6..0000000 --- a/cmd/backend/server/middleware/request_log.go +++ /dev/null @@ -1,56 +0,0 @@ -package middleware - -import ( - "backend/internal/integrations" - log "backend/pkg/logger" - "time" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "go.opentelemetry.io/otel/trace" -) - -func NewRequestLogMiddleware(logger log.Logger, tracer trace.Tracer, prometheus *integrations.Prometheus) gin.HandlerFunc { - return func(c *gin.Context) { - prometheus.RequestInc() - defer prometheus.RequestDec() - - requestId := c.GetHeader("X-Request-Id") - if requestId == "" { - requestId = uuid.New().String() - } - c.Header("X-Request-Id", requestId) - c.Header("Access-Control-Allow-Origin", "*") - - log.SetCtxRequestId(c, requestId) - - path := c.Request.URL.Path - if c.Request.URL.RawQuery != "" { - path = path + "?" + c.Request.URL.RawQuery - } - - start := time.Now() - c.Next() - latency := time.Since(start) - - prometheus.AddRequestTime(float64(latency.Microseconds())) - - method := c.Request.Method - statusCode := c.Writer.Status() - - if statusCode >= 200 && statusCode < 400 { - return - } - - ctxLogger := logger.WithContext(c) - - if statusCode >= 400 && statusCode < 500 { - prometheus.Add4xxError() - ctxLogger.Warning().Msgf("Request %s %s %d %v", method, path, statusCode, latency) - return - } - - prometheus.Add5xxError() - ctxLogger.Error().Msgf("Request %s %s %d %v", method, path, statusCode, latency) - } -} diff --git a/cmd/backend/server/server.go b/cmd/backend/server/server.go index e968048..9c70bc4 100644 --- a/cmd/backend/server/server.go +++ b/cmd/backend/server/server.go @@ -5,21 +5,14 @@ import ( "backend/cmd/backend/server/middleware" "backend/cmd/backend/server/utils" "backend/internal/core/services" + httpserver "backend/internal/http_server" "backend/internal/integrations" "backend/pkg/logger" - "context" - "fmt" - "net" "github.com/gin-gonic/gin" "go.opentelemetry.io/otel/trace" ) -type Server struct { - logger logger.Logger - ginEngine *gin.Engine -} - type NewServerOpts struct { DebugMode bool Logger logger.Logger @@ -28,7 +21,7 @@ type NewServerOpts struct { Tracer trace.Tracer } -func New(opts NewServerOpts) *Server { +func NewServer(opts NewServerOpts) *httpserver.Server { if !opts.DebugMode { gin.SetMode(gin.ReleaseMode) } @@ -42,9 +35,9 @@ func New(opts NewServerOpts) *Server { prometheus := integrations.NewPrometheus() r.Any("/metrics", gin.WrapH(prometheus.GetRequestHandler())) - r.Use(middleware.NewRecoveryMiddleware(opts.Logger, prometheus, opts.DebugMode)) - r.Use(middleware.NewRequestLogMiddleware(opts.Logger, opts.Tracer, prometheus)) - r.Use(middleware.NewTracingMiddleware(opts.Tracer)) + r.Use(httpserver.NewRecoveryMiddleware(opts.Logger, prometheus, opts.DebugMode)) + r.Use(httpserver.NewRequestLogMiddleware(opts.Logger, opts.Tracer, prometheus)) + r.Use(httpserver.NewTracingMiddleware(opts.Tracer)) userGroup := r.Group("/user") userGroup.POST("/create", handlers.NewUserCreateHandler(opts.Logger, opts.UserService)) @@ -60,29 +53,10 @@ func New(opts NewServerOpts) *Server { }) } - return &Server{ - logger: opts.Logger, - ginEngine: r, - } -} - -func (s *Server) Run(ctx context.Context, port uint16) { - listenAddr := fmt.Sprintf("0.0.0.0:%d", port) - s.logger.Log().Msgf("server listening on %s", listenAddr) - - listener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", listenAddr) - if err != nil { - s.logger.Fatal().Err(err).Msg("can not create network listener") - } - - go func() { - <-ctx.Done() - s.logger.Log().Msg("stopping tcp listener...") - listener.Close() - }() - - err = s.ginEngine.RunListener(listener) - if err != nil && err == net.ErrClosed { - s.logger.Fatal().Err(err).Msg("server stopped with error") - } + return httpserver.New( + httpserver.NewServerOpts{ + Logger: opts.Logger, + HttpServer: r, + }, + ) } diff --git a/cmd/shortlinks/main.go b/cmd/shortlinks/main.go index 4fb9324..2adbf3d 100644 --- a/cmd/shortlinks/main.go +++ b/cmd/shortlinks/main.go @@ -6,7 +6,6 @@ import ( grpcserver "backend/internal/grpc_server" "backend/internal/grpc_server/shortlinks" httpserver "backend/internal/http_server" - "backend/internal/http_server/middleware" "backend/internal/integrations" "backend/pkg/cache" "backend/pkg/logger" @@ -94,9 +93,9 @@ func RunServer(ctx context.Context, log logger.Logger, tracer trace.Tracer, conf ctx.Status(200) }) - r.Use(middleware.NewRecoveryMiddleware(log, prometheus, debugMode)) - r.Use(middleware.NewRequestLogMiddleware(log, tracer, prometheus)) - r.Use(middleware.NewTracingMiddleware(tracer)) + r.Use(httpserver.NewRecoveryMiddleware(log, prometheus, debugMode)) + r.Use(httpserver.NewRequestLogMiddleware(log, tracer, prometheus)) + r.Use(httpserver.NewTracingMiddleware(tracer)) linkGroup := r.Group("/s") linkGroup.POST("/new", NewShortlinkCreateHandler(log, shortlinkService, host)) diff --git a/internal/http_server/middleware/recovery.go b/internal/http_server/middleware/recovery.go deleted file mode 100644 index 472e126..0000000 --- a/internal/http_server/middleware/recovery.go +++ /dev/null @@ -1,155 +0,0 @@ -package middleware - -// Modified recovery from gin, use own logger - -import ( - "backend/internal/integrations" - "backend/pkg/logger" - "bytes" - "errors" - "fmt" - "net" - "net/http" - "net/http/httputil" - "os" - "runtime" - "strings" - "time" - - "github.com/gin-gonic/gin" -) - -const ( - reset = "\033[0m" -) - -var ( - dunno = []byte("???") - centerDot = []byte("·") - dot = []byte(".") - slash = []byte("/") -) - -func NewRecoveryMiddleware(logger logger.Logger, prometheus *integrations.Prometheus, debugMode bool) gin.HandlerFunc { - handle := defaultHandleRecovery - return func(c *gin.Context) { - defer func() { - if err := recover(); err != nil { - prometheus.AddPanic() - - // Check for a broken connection, as it is not really a - // condition that warrants a panic stack trace. - var brokenPipe bool - if ne, ok := err.(*net.OpError); ok { - var se *os.SyscallError - if errors.As(ne, &se) { - seStr := strings.ToLower(se.Error()) - if strings.Contains(seStr, "broken pipe") || - strings.Contains(seStr, "connection reset by peer") { - brokenPipe = true - } - } - } - if logger != nil { - stack := stack(3) - httpRequest, _ := httputil.DumpRequest(c.Request, false) - headers := strings.Split(string(httpRequest), "\r\n") - for idx, header := range headers { - current := strings.Split(header, ":") - if current[0] == "Authorization" { - headers[idx] = current[0] + ": *" - } - } - headersToStr := strings.Join(headers, "\r\n") - if brokenPipe { - logger.Printf("%s\n%s%s", err, headersToStr, reset) - } else if debugMode { - logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", - timeFormat(time.Now()), headersToStr, err, stack, reset) - } else { - logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", - timeFormat(time.Now()), err, stack, reset) - } - } - if brokenPipe { - // If the connection is dead, we can't write a status to it. - c.Error(err.(error)) //nolint: errcheck - c.Abort() - } else { - handle(c, err) - } - } - }() - c.Next() - } -} - -func defaultHandleRecovery(c *gin.Context, _ any) { - c.AbortWithStatus(http.StatusInternalServerError) -} - -// stack returns a nicely formatted stack frame, skipping skip frames. -func stack(skip int) []byte { - buf := new(bytes.Buffer) // the returned data - // As we loop, we open files and read them. These variables record the currently - // loaded file. - var lines [][]byte - var lastFile string - for i := skip; ; i++ { // Skip the expected number of frames - pc, file, line, ok := runtime.Caller(i) - if !ok { - break - } - // Print this much at least. If we can't find the source, it won't show. - fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc) - if file != lastFile { - data, err := os.ReadFile(file) - if err != nil { - continue - } - lines = bytes.Split(data, []byte{'\n'}) - lastFile = file - } - fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line)) - } - return buf.Bytes() -} - -// source returns a space-trimmed slice of the n'th line. -func source(lines [][]byte, n int) []byte { - n-- // in stack trace, lines are 1-indexed but our array is 0-indexed - if n < 0 || n >= len(lines) { - return dunno - } - return bytes.TrimSpace(lines[n]) -} - -// function returns, if possible, the name of the function containing the PC. -func function(pc uintptr) []byte { - fn := runtime.FuncForPC(pc) - if fn == nil { - return dunno - } - name := []byte(fn.Name()) - // The name includes the path name to the package, which is unnecessary - // since the file name is already included. Plus, it has center dots. - // That is, we see - // runtime/debug.*T·ptrmethod - // and want - // *T.ptrmethod - // Also the package path might contain dot (e.g. code.google.com/...), - // so first eliminate the path prefix - if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 { - name = name[lastSlash+1:] - } - if period := bytes.Index(name, dot); period >= 0 { - name = name[period+1:] - } - name = bytes.ReplaceAll(name, centerDot, dot) - return name -} - -// timeFormat returns a customized time string for logger. -func timeFormat(t time.Time) string { - return t.Format("2006/01/02 - 15:04:05") -} diff --git a/internal/http_server/middleware/tracing.go b/internal/http_server/middleware/tracing.go deleted file mode 100644 index a739c3c..0000000 --- a/internal/http_server/middleware/tracing.go +++ /dev/null @@ -1,33 +0,0 @@ -package middleware - -import ( - "fmt" - - "github.com/gin-gonic/gin" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace" -) - -func NewTracingMiddleware(tracer trace.Tracer) gin.HandlerFunc { - prop := otel.GetTextMapPropagator() - - return func(c *gin.Context) { - savedCtx := c.Request.Context() - defer func() { - c.Request = c.Request.WithContext(savedCtx) - }() - - ctx := prop.Extract(savedCtx, propagation.HeaderCarrier(c.Request.Header)) - - ctx, span := tracer.Start(ctx, fmt.Sprintf("%s %s", c.Request.Method, c.Request.URL.Path)) - defer span.End() - - traceId := span.SpanContext().TraceID() - c.Header("X-Trace-Id", traceId.String()) - - c.Request = c.Request.WithContext(ctx) - - c.Next() - } -} diff --git a/cmd/backend/server/middleware/recovery.go b/internal/http_server/recovery.go similarity index 99% rename from cmd/backend/server/middleware/recovery.go rename to internal/http_server/recovery.go index 472e126..4611dab 100644 --- a/cmd/backend/server/middleware/recovery.go +++ b/internal/http_server/recovery.go @@ -1,4 +1,4 @@ -package middleware +package httpserver // Modified recovery from gin, use own logger diff --git a/internal/http_server/middleware/request_log.go b/internal/http_server/request_log.go similarity index 98% rename from internal/http_server/middleware/request_log.go rename to internal/http_server/request_log.go index 814d86e..45123ca 100644 --- a/internal/http_server/middleware/request_log.go +++ b/internal/http_server/request_log.go @@ -1,4 +1,4 @@ -package middleware +package httpserver import ( "backend/internal/integrations" diff --git a/cmd/backend/server/middleware/tracing.go b/internal/http_server/tracing.go similarity index 97% rename from cmd/backend/server/middleware/tracing.go rename to internal/http_server/tracing.go index a739c3c..9781378 100644 --- a/cmd/backend/server/middleware/tracing.go +++ b/internal/http_server/tracing.go @@ -1,4 +1,4 @@ -package middleware +package httpserver import ( "fmt" From 64bb402ad1737378f61435bbe0c3b6267db2e2c8 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Fri, 7 Feb 2025 13:56:35 +0300 Subject: [PATCH 6/9] add v2 prefix to current backend api --- cmd/backend/server/server.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cmd/backend/server/server.go b/cmd/backend/server/server.go index 9c70bc4..50f7877 100644 --- a/cmd/backend/server/server.go +++ b/cmd/backend/server/server.go @@ -29,7 +29,7 @@ func NewServer(opts NewServerOpts) *httpserver.Server { r := gin.New() r.ContextWithFallback = true // Use it to allow getting values from c.Request.Context() - r.Static("/webapp", "./webapp") + // r.Static("/webapp", "./webapp") r.GET("/health", handlers.NewDummyHandler()) prometheus := integrations.NewPrometheus() @@ -39,11 +39,15 @@ func NewServer(opts NewServerOpts) *httpserver.Server { r.Use(httpserver.NewRequestLogMiddleware(opts.Logger, opts.Tracer, prometheus)) r.Use(httpserver.NewTracingMiddleware(opts.Tracer)) - userGroup := r.Group("/user") - userGroup.POST("/create", handlers.NewUserCreateHandler(opts.Logger, opts.UserService)) - userGroup.POST("/login", handlers.NewUserLoginHandler(opts.Logger, opts.UserService)) + v1 := r.Group("/v1") - dummyGroup := r.Group("/dummy") + userGroup := v1.Group("/user") + { + userGroup.POST("/create", handlers.NewUserCreateHandler(opts.Logger, opts.UserService)) + userGroup.POST("/login", handlers.NewUserLoginHandler(opts.Logger, opts.UserService)) + } + + dummyGroup := v1.Group("/dummy") { dummyGroup.Use(middleware.NewAuthMiddleware(opts.UserService)) dummyGroup.GET("", handlers.NewDummyHandler()) From 1126ff132128b13595d430dbd619d32659619b68 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Fri, 7 Feb 2025 17:12:59 +0300 Subject: [PATCH 7/9] add integration tests --- {load_tests => tests}/.gitignore | 0 {load_tests => tests}/api.py | 30 ++++++------------- tests/integration/test_user.py | 12 ++++++++ {load_tests => tests}/makefile | 2 +- .../tests => tests/performance}/dummy.py | 12 ++++---- .../tests => tests/performance}/health.py | 8 ++--- .../loads => tests/performance}/low_load.py | 6 ++-- .../tests => tests/performance}/shortlink.py | 8 ++--- .../tests => tests/performance}/user.py | 17 ++++------- {load_tests => tests}/requirements.txt | 5 ++++ 10 files changed, 47 insertions(+), 53 deletions(-) rename {load_tests => tests}/.gitignore (100%) rename {load_tests => tests}/api.py (70%) create mode 100644 tests/integration/test_user.py rename {load_tests => tests}/makefile (83%) rename {load_tests/tests => tests/performance}/dummy.py (65%) rename {load_tests/tests => tests/performance}/health.py (65%) rename {load_tests/loads => tests/performance}/low_load.py (83%) rename {load_tests/tests => tests/performance}/shortlink.py (61%) rename {load_tests/tests => tests/performance}/user.py (66%) rename {load_tests => tests}/requirements.txt (84%) diff --git a/load_tests/.gitignore b/tests/.gitignore similarity index 100% rename from load_tests/.gitignore rename to tests/.gitignore diff --git a/load_tests/api.py b/tests/api.py similarity index 70% rename from load_tests/api.py rename to tests/api.py index d41fc4c..c945007 100644 --- a/load_tests/api.py +++ b/tests/api.py @@ -9,6 +9,7 @@ class Auth(): self.token = token class User(): + id: string email: string name: string password: string @@ -21,18 +22,16 @@ class User(): class BackendApi(): - http: FastHttpUser - - def __init__(self, http: FastHttpUser): - self.http = http + def __init__(self, httpClient): + self.httpClient = httpClient def user_create(self) -> User: email = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + '@test.test' name = ''.join(random.choices(string.ascii_letters, k=10)) password = 'Abcdef1!!1' - response = self.http.client.post( - "/user/create", + response = self.httpClient.post( + "/v1/user/create", json={ "email": email, "password": password, @@ -45,8 +44,8 @@ class BackendApi(): return User(email, password, name) def user_login(self, user: User) -> Auth: - response = self.http.client.post( - "/user/login", + response = self.httpClient.post( + "/v1/user/login", json={ "email": user.email, "password": user.password, @@ -63,22 +62,11 @@ class BackendApi(): def dummy_get(self, auth: Auth): headers = {"X-Auth": auth.token} - response = self.http.client.get("/dummy", headers=headers) + response = self.httpClient.get("/v1/dummy", headers=headers) if response.status_code != 200: raise AssertionError('something wrong') def health_get(self): - response = self.http.client.get("/health") + response = self.httpClient.get("/health") if response.status_code != 200: raise AssertionError('something wrong') - - def shortlink_create(self, url: string) -> string: - response = self.http.client.post("/s/new?url=" + url) - if response.status_code != 200: - raise AssertionError('can not login user') - - link = response.json()['link'] - if link == '': - raise AssertionError('empty user token') - - return link \ No newline at end of file diff --git a/tests/integration/test_user.py b/tests/integration/test_user.py new file mode 100644 index 0000000..9c69bc6 --- /dev/null +++ b/tests/integration/test_user.py @@ -0,0 +1,12 @@ +from api import BackendApi +import requests + +class TestUser: + def test_create_user(self): + api = BackendApi(requests) + api.user_create() + + def test_login_user(self): + api = BackendApi(requests) + user = api.user_create() + api.user_login(user) \ No newline at end of file diff --git a/load_tests/makefile b/tests/makefile similarity index 83% rename from load_tests/makefile rename to tests/makefile index efc9b49..b349abd 100644 --- a/load_tests/makefile +++ b/tests/makefile @@ -16,6 +16,6 @@ requirements: pip freeze > requirements.txt run-web: - locust -f tests,loads --class-picker --host http://localhost:8080 --processes 16 + locust -f performance --class-picker --host http://localhost:8080 --processes 16 diff --git a/load_tests/tests/dummy.py b/tests/performance/dummy.py similarity index 65% rename from load_tests/tests/dummy.py rename to tests/performance/dummy.py index bf84587..8d5f08d 100644 --- a/load_tests/tests/dummy.py +++ b/tests/performance/dummy.py @@ -3,15 +3,13 @@ from locust import FastHttpUser, task from api import BackendApi, Auth class DummyGet(FastHttpUser): - api: BackendApi - auth: Auth + def on_start(self): + self.api = BackendApi(self.client) + user = self.api.user_create() + self.auth = self.api.user_login(user) @task def dummy_test(self): self.api.dummy_get(self.auth) - def on_start(self): - self.api = BackendApi(self) - - user = self.api.user_create() - self.auth = self.api.user_login(user) \ No newline at end of file + \ No newline at end of file diff --git a/load_tests/tests/health.py b/tests/performance/health.py similarity index 65% rename from load_tests/tests/health.py rename to tests/performance/health.py index df5ddc5..8c05304 100644 --- a/load_tests/tests/health.py +++ b/tests/performance/health.py @@ -3,11 +3,9 @@ from locust import FastHttpUser, task from api import BackendApi class HealthGet(FastHttpUser): - api: BackendApi + def on_start(self): + self.api = BackendApi(self.client) @task def user_create_test(self): - self.api.health_get() - - def on_start(self): - self.api = BackendApi(self) \ No newline at end of file + self.api.health_get() \ No newline at end of file diff --git a/load_tests/loads/low_load.py b/tests/performance/low_load.py similarity index 83% rename from load_tests/loads/low_load.py rename to tests/performance/low_load.py index c84d101..722a030 100644 --- a/load_tests/loads/low_load.py +++ b/tests/performance/low_load.py @@ -1,9 +1,9 @@ from locust import LoadTestShape class LowLoad(LoadTestShape): - time_limit = 600 - spawn_rate = 5 - max_users = 100 + time_limit = 60 + spawn_rate = 2 + max_users = 10 def tick(self) -> (tuple[float, int] | None): user_count = self.spawn_rate * self.get_run_time() diff --git a/load_tests/tests/shortlink.py b/tests/performance/shortlink.py similarity index 61% rename from load_tests/tests/shortlink.py rename to tests/performance/shortlink.py index 2f9072c..8fb0e46 100644 --- a/load_tests/tests/shortlink.py +++ b/tests/performance/shortlink.py @@ -3,11 +3,9 @@ from locust import FastHttpUser, task from api import BackendApi class ShortlinkCreate(FastHttpUser): - api: BackendApi + def on_start(self): + self.api = BackendApi(self.client) @task def user_create_test(self): - self.api.shortlink_create("https://ya.ru") - - def on_start(self): - self.api = BackendApi(self) \ No newline at end of file + self.api.shortlink_create("https://example.com") \ No newline at end of file diff --git a/load_tests/tests/user.py b/tests/performance/user.py similarity index 66% rename from load_tests/tests/user.py rename to tests/performance/user.py index d9f3881..e67b067 100644 --- a/load_tests/tests/user.py +++ b/tests/performance/user.py @@ -3,23 +3,18 @@ from locust import FastHttpUser, task from api import BackendApi, User class UserCreate(FastHttpUser): - api: BackendApi + def on_start(self): + self.api = BackendApi(self.client) @task def user_create_test(self): self.api.user_create() - def on_start(self): - self.api = BackendApi(self) - class UserLogin(FastHttpUser): - api: BackendApi - user: User + def on_start(self): + self.api = BackendApi(self) + self.user = self.api.user_create() @task def user_create_test(self): - self.api.user_login(self.user) - - def on_start(self): - self.api = BackendApi(self) - self.user = self.api.user_create() \ No newline at end of file + self.api.user_login(self.user) \ No newline at end of file diff --git a/load_tests/requirements.txt b/tests/requirements.txt similarity index 84% rename from load_tests/requirements.txt rename to tests/requirements.txt index 166fda7..ea6536c 100644 --- a/load_tests/requirements.txt +++ b/tests/requirements.txt @@ -4,6 +4,7 @@ certifi==2024.7.4 charset-normalizer==3.3.2 click==8.1.7 ConfigArgParse==1.7 +exceptiongroup==1.2.2 Flask==3.0.3 Flask-Cors==4.0.1 Flask-Login==0.6.3 @@ -11,12 +12,16 @@ gevent==24.2.1 geventhttpclient==2.3.1 greenlet==3.0.3 idna==3.7 +iniconfig==2.0.0 itsdangerous==2.2.0 Jinja2==3.1.4 locust==2.31.3 MarkupSafe==2.1.5 msgpack==1.0.8 +packaging==24.2 +pluggy==1.5.0 psutil==6.0.0 +pytest==8.3.4 pyzmq==26.1.0 requests==2.32.3 tomli==2.0.1 From 2ede1685183926cb1dfbddf23b690aab4244ea3a Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Fri, 7 Feb 2025 18:22:41 +0300 Subject: [PATCH 8/9] fix tests --- tests/api.py | 20 ++++++++++++++++---- tests/integration/test_user.py | 8 +++++--- tests/makefile | 5 ++++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/tests/api.py b/tests/api.py index c945007..338e043 100644 --- a/tests/api.py +++ b/tests/api.py @@ -1,6 +1,13 @@ import random import string -from locust import HttpUser, FastHttpUser +import requests + +class Requests(): + def __init__(self, baseUrl): + self.baseUrl = baseUrl + + def post(self, path, json = {}): + return requests.post(self.baseUrl + path, json=json) class Auth(): token: string @@ -47,14 +54,19 @@ class BackendApi(): response = self.httpClient.post( "/v1/user/login", json={ - "email": user.email, + "email": user.email+"a", "password": user.password, }, ) + + status = response.json()['status'] + if status == 'error': + raise AssertionError(response.json()['error']['message']) + if response.status_code != 200: raise AssertionError('can not login user') - - token = response.json()['token'] + + token = response.json()['result']['token'] if token == '': raise AssertionError('empty user token') diff --git a/tests/integration/test_user.py b/tests/integration/test_user.py index 9c69bc6..0614b82 100644 --- a/tests/integration/test_user.py +++ b/tests/integration/test_user.py @@ -1,12 +1,14 @@ -from api import BackendApi +from api import BackendApi, Requests import requests +backendUrl = "http://localhost:8080" + class TestUser: def test_create_user(self): - api = BackendApi(requests) + api = BackendApi(Requests(backendUrl)) api.user_create() def test_login_user(self): - api = BackendApi(requests) + api = BackendApi(Requests(backendUrl)) user = api.user_create() api.user_login(user) \ No newline at end of file diff --git a/tests/makefile b/tests/makefile index b349abd..36f1721 100644 --- a/tests/makefile +++ b/tests/makefile @@ -15,7 +15,10 @@ install: requirements: pip freeze > requirements.txt -run-web: +run-integration: + python3 -m pytest integration/ + +run-performance-web: locust -f performance --class-picker --host http://localhost:8080 --processes 16 From d68abec2e5f41caa4fc6838f57cef021e87eca17 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Fri, 7 Feb 2025 21:35:21 +0300 Subject: [PATCH 9/9] fixes for python api --- tests/api.py | 74 +++++++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/tests/api.py b/tests/api.py index 338e043..d9d1738 100644 --- a/tests/api.py +++ b/tests/api.py @@ -21,7 +21,7 @@ class User(): name: string password: string - def __init__(self, email, password, name, token = ""): + def __init__(self, email, password, name, id="", token = ""): self.email = email self.password = password self.name = name @@ -32,45 +32,49 @@ class BackendApi(): def __init__(self, httpClient): self.httpClient = httpClient - def user_create(self) -> User: - email = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + '@test.test' - name = ''.join(random.choices(string.ascii_letters, k=10)) - password = 'Abcdef1!!1' - - response = self.httpClient.post( - "/v1/user/create", - json={ - "email": email, - "password": password, - "name": name, - }, - ) - if response.status_code != 200: - raise AssertionError('can not create user') + def parse_response(self, response): + if response.status != 200: + raise AssertionError('something wrong') - return User(email, password, name) + json = response.json() + if json['status'] == 'success': + if 'result' in json: + return json['result'] + return None + + error = json['error'] + raise AssertionError(error['id'], error['message']) + + def user_create(self, user: User | None) -> User: + if user == None: + email = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + '@test.test' + name = ''.join(random.choices(string.ascii_letters, k=10)) + password = 'Abcdef1!!1' + user = User(email, password, name) + + res = self.parse_response( + self.httpClient.post( + "/v1/user/create", json={ + "email": user.email, + "password": user.password, + "name": user.name, + } + ) + ) + + return User(res['email'], res['password'], res['name'], res['id']) def user_login(self, user: User) -> Auth: - response = self.httpClient.post( - "/v1/user/login", - json={ - "email": user.email+"a", - "password": user.password, - }, + res = self.parse_response( + self.httpClient.post( + "/v1/user/login", json={ + "email": user.email+"a", + "password": user.password, + }, + ) ) - - status = response.json()['status'] - if status == 'error': - raise AssertionError(response.json()['error']['message']) - - if response.status_code != 200: - raise AssertionError('can not login user') - - token = response.json()['result']['token'] - if token == '': - raise AssertionError('empty user token') - return Auth(token) + return Auth(res['status']) def dummy_get(self, auth: Auth): headers = {"X-Auth": auth.token}