diff --git a/Makefile b/Makefile index af5143d..f236185 100644 --- a/Makefile +++ b/Makefile @@ -122,6 +122,22 @@ mock: -destination=internal/app/auth/mock/auth.go \ github.com/ozontech/seq-ui/internal/app/auth \ OIDCProvider,JWTProvider + PATH="$(LOCAL_BIN):$(PATH)" mockgen \ + -destination=internal/pkg/service/dashboards/mock/service.go \ + github.com/ozontech/seq-ui/internal/pkg/service/dashboards \ + Service + PATH="$(LOCAL_BIN):$(PATH)" mockgen \ + -destination=internal/pkg/service/async_searches/mock/service.go \ + github.com/ozontech/seq-ui/internal/pkg/service/async_searches \ + Service + PATH="$(LOCAL_BIN):$(PATH)" mockgen \ + -destination=internal/pkg/service/userprofile/mock/service.go \ + github.com/ozontech/seq-ui/internal/pkg/service/userprofile \ + Service + PATH="$(LOCAL_BIN):$(PATH)" mockgen \ + -destination=internal/pkg/service/massexport/mock/service.go \ + github.com/ozontech/seq-ui/internal/pkg/service/massexport \ + Service .PHONY: protoc protoc: diff --git a/cmd/seq-ui/main.go b/cmd/seq-ui/main.go index 9450352..bb3c7cc 100644 --- a/cmd/seq-ui/main.go +++ b/cmd/seq-ui/main.go @@ -21,7 +21,6 @@ import ( dashboards_v1 "github.com/ozontech/seq-ui/internal/api/dashboards/v1" errorgroups_v1 "github.com/ozontech/seq-ui/internal/api/errorgroups/v1" massexport_v1 "github.com/ozontech/seq-ui/internal/api/massexport/v1" - "github.com/ozontech/seq-ui/internal/api/profiles" seqapi_v1 "github.com/ozontech/seq-ui/internal/api/seqapi/v1" userprofile_v1 "github.com/ozontech/seq-ui/internal/api/userprofile/v1" "github.com/ozontech/seq-ui/internal/app/config" @@ -30,12 +29,14 @@ import ( "github.com/ozontech/seq-ui/internal/pkg/client/seqdb" "github.com/ozontech/seq-ui/internal/pkg/repository" repositorych "github.com/ozontech/seq-ui/internal/pkg/repository_ch" - "github.com/ozontech/seq-ui/internal/pkg/service" asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches" + "github.com/ozontech/seq-ui/internal/pkg/service/dashboards" "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups" "github.com/ozontech/seq-ui/internal/pkg/service/massexport" "github.com/ozontech/seq-ui/internal/pkg/service/massexport/filestore" "github.com/ozontech/seq-ui/internal/pkg/service/massexport/sessionstore" + "github.com/ozontech/seq-ui/internal/pkg/service/profiles" + "github.com/ozontech/seq-ui/internal/pkg/service/userprofile" "github.com/ozontech/seq-ui/logger" "github.com/ozontech/seq-ui/tracing" ) @@ -151,23 +152,23 @@ func initApp(ctx context.Context, cfg config.Config) *api.Registrar { } var ( - asyncSearchesService *asyncsearches.Service - p *profiles.Profiles + asyncSearchesService asyncsearches.Service userProfileV1 *userprofile_v1.UserProfile dashboardsV1 *dashboards_v1.Dashboards ) if db != nil { repo := repository.New(db, cfg.Server.DB.RequestTimeout) - svc := service.New(repo) - p = profiles.New(svc) + userProfilesSvc := userprofile.New(repo.UserProfiles, repo.FavoriteQueries) + dashboardsSvc := dashboards.New(repo.Dashboards) + profiles.InitProfiles(userProfilesSvc) - userProfileV1 = userprofile_v1.New(svc, p) - dashboardsV1 = dashboards_v1.New(svc, p) + userProfileV1 = userprofile_v1.New(userProfilesSvc) + dashboardsV1 = dashboards_v1.New(dashboardsSvc) asyncSearchesService = asyncsearches.New(ctx, repo, defaultClient, cfg.Handlers.AsyncSearch) } - seqApiV1 := seqapi_v1.New(cfg.Handlers.SeqAPI, seqDBClients, inmemWithRedisCache, redisCache, asyncSearchesService, p) + seqApiV1 := seqapi_v1.New(cfg.Handlers.SeqAPI, seqDBClients, inmemWithRedisCache, redisCache, asyncSearchesService) logger.Info("initializing clickhouse") ch, err := initClickHouse(ctx, cfg.Server.CH) diff --git a/internal/api/dashboards/v1/dashboards.go b/internal/api/dashboards/v1/dashboards.go index cdb62f1..ec0031e 100644 --- a/internal/api/dashboards/v1/dashboards.go +++ b/internal/api/dashboards/v1/dashboards.go @@ -5,8 +5,7 @@ import ( grpc_api "github.com/ozontech/seq-ui/internal/api/dashboards/v1/grpc" http_api "github.com/ozontech/seq-ui/internal/api/dashboards/v1/http" - "github.com/ozontech/seq-ui/internal/api/profiles" - "github.com/ozontech/seq-ui/internal/pkg/service" + "github.com/ozontech/seq-ui/internal/pkg/service/dashboards" ) type Dashboards struct { @@ -14,10 +13,10 @@ type Dashboards struct { httpAPI *http_api.API } -func New(svc service.Service, p *profiles.Profiles) *Dashboards { +func New(svc dashboards.Service) *Dashboards { return &Dashboards{ - grpcAPI: grpc_api.New(svc, p), - httpAPI: http_api.New(svc, p), + grpcAPI: grpc_api.New(svc), + httpAPI: http_api.New(svc), } } diff --git a/internal/api/dashboards/v1/grpc/api.go b/internal/api/dashboards/v1/grpc/api.go index 7e59b4e..2422d0b 100644 --- a/internal/api/dashboards/v1/grpc/api.go +++ b/internal/api/dashboards/v1/grpc/api.go @@ -1,21 +1,18 @@ package grpc import ( - "github.com/ozontech/seq-ui/internal/api/profiles" - "github.com/ozontech/seq-ui/internal/pkg/service" - "github.com/ozontech/seq-ui/pkg/dashboards/v1" + "github.com/ozontech/seq-ui/internal/pkg/service/dashboards" + api "github.com/ozontech/seq-ui/pkg/dashboards/v1" ) type API struct { - dashboards.UnimplementedDashboardsServiceServer + api.UnimplementedDashboardsServiceServer - service service.Service - profiles *profiles.Profiles + service dashboards.Service } -func New(svc service.Service, p *profiles.Profiles) *API { +func New(svc dashboards.Service) *API { return &API{ - service: svc, - profiles: p, + service: svc, } } diff --git a/internal/api/dashboards/v1/grpc/create.go b/internal/api/dashboards/v1/grpc/create.go index b6f09cf..66e44a0 100644 --- a/internal/api/dashboards/v1/grpc/create.go +++ b/internal/api/dashboards/v1/grpc/create.go @@ -22,16 +22,11 @@ func (a *API) Create(ctx context.Context, req *dashboards.CreateRequest) (*dashb }, ) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - return nil, grpcutil.ProcessError(err) - } - request := types.CreateDashboardRequest{ - ProfileID: profileID, - Name: req.Name, - Meta: req.Meta, + Name: req.Name, + Meta: req.Meta, } + uuid, err := a.service.CreateDashboard(ctx, request) if err != nil { return nil, grpcutil.ProcessError(err) diff --git a/internal/api/dashboards/v1/grpc/create_test.go b/internal/api/dashboards/v1/grpc/create_test.go index 30a5c25..d143d57 100644 --- a/internal/api/dashboards/v1/grpc/create_test.go +++ b/internal/api/dashboards/v1/grpc/create_test.go @@ -2,7 +2,6 @@ package grpc import ( "context" - "errors" "testing" "github.com/stretchr/testify/require" @@ -15,12 +14,6 @@ import ( ) func TestCreate(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - dashboardUUID := "064dc707-02b8-7000-8201-02a7f396738a" - dashboardName := "my_dashboard" - dashboardMeta := "my_meta" - type mockArgs struct { req types.CreateDashboardRequest resp string @@ -35,82 +28,56 @@ func TestCreate(t *testing.T) { wantCode codes.Code mockArgs *mockArgs - noUser bool }{ { - name: "success", + name: "ok", req: &dashboards.CreateRequest{ - Name: dashboardName, - Meta: dashboardMeta, + Name: testDashboardName, + Meta: testDashboardMeta, }, want: &dashboards.CreateResponse{ - Uuid: dashboardUUID, + Uuid: testDashboardUUID, }, wantCode: codes.OK, mockArgs: &mockArgs{ req: types.CreateDashboardRequest{ - ProfileID: profileID, - Name: dashboardName, - Meta: dashboardMeta, + Name: testDashboardName, + Meta: testDashboardMeta, }, - resp: dashboardUUID, - }, - }, - { - name: "err_no_user", - wantCode: codes.Unauthenticated, - noUser: true, - }, - { - name: "err_svc_empty_name", - req: &dashboards.CreateRequest{ - Meta: dashboardMeta, + resp: testDashboardUUID, }, - wantCode: codes.InvalidArgument, }, { - name: "err_svc_empty_meta", + name: "err_svc", req: &dashboards.CreateRequest{ - Name: dashboardName, - }, - wantCode: codes.InvalidArgument, - }, - { - name: "err_repo_random", - req: &dashboards.CreateRequest{ - Name: dashboardName, - Meta: dashboardMeta, + Name: testDashboardName, + Meta: testDashboardMeta, }, wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.CreateDashboardRequest{ - ProfileID: profileID, - Name: dashboardName, - Meta: dashboardMeta, + Name: testDashboardName, + Meta: testDashboardMeta, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newTestData(t) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().Create(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - - ctx := context.Background() - if !tt.noUser { - ctx = context.WithValue(ctx, types.UserKey{}, userName) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + CreateDashboard(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - got, err := api.Create(ctx, tt.req) + got, err := api.Create(context.Background(), tt.req) require.Equal(t, tt.wantCode, status.Code(err)) if tt.wantCode != codes.OK { diff --git a/internal/api/dashboards/v1/grpc/delete.go b/internal/api/dashboards/v1/grpc/delete.go index e2408f3..5d286a7 100644 --- a/internal/api/dashboards/v1/grpc/delete.go +++ b/internal/api/dashboards/v1/grpc/delete.go @@ -22,16 +22,11 @@ func (a *API) Delete(ctx context.Context, req *dashboards.DeleteRequest) (*dashb }, ) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - return nil, grpcutil.ProcessError(err) - } - request := types.DeleteDashboardRequest{ - UUID: req.Uuid, - ProfileID: profileID, + UUID: req.Uuid, } - if err = a.service.DeleteDashboard(ctx, request); err != nil { + + if err := a.service.DeleteDashboard(ctx, request); err != nil { return nil, grpcutil.ProcessError(err) } diff --git a/internal/api/dashboards/v1/grpc/delete_test.go b/internal/api/dashboards/v1/grpc/delete_test.go index 9401f59..8cd9a9c 100644 --- a/internal/api/dashboards/v1/grpc/delete_test.go +++ b/internal/api/dashboards/v1/grpc/delete_test.go @@ -2,7 +2,6 @@ package grpc import ( "context" - "errors" "testing" "github.com/stretchr/testify/require" @@ -15,10 +14,6 @@ import ( ) func TestDelete(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - dashboardUUID := "064dc707-02b8-7000-8201-02a7f396738a" - type mockArgs struct { req types.DeleteDashboardRequest err error @@ -32,82 +27,49 @@ func TestDelete(t *testing.T) { wantCode codes.Code mockArgs *mockArgs - noUser bool }{ { - name: "success", + name: "ok", req: &dashboards.DeleteRequest{ - Uuid: dashboardUUID, + Uuid: testDashboardUUID, }, want: &dashboards.DeleteResponse{}, wantCode: codes.OK, mockArgs: &mockArgs{ req: types.DeleteDashboardRequest{ - UUID: dashboardUUID, - ProfileID: profileID, - }, - }, - }, - { - name: "err_no_user", - wantCode: codes.Unauthenticated, - noUser: true, - }, - { - name: "err_svc_invalid_uuid", - req: &dashboards.DeleteRequest{ - Uuid: "invalid-uuid", - }, - wantCode: codes.InvalidArgument, - }, - { - name: "err_repo_permission_denied", - req: &dashboards.DeleteRequest{ - Uuid: dashboardUUID, - }, - wantCode: codes.PermissionDenied, - mockArgs: &mockArgs{ - req: types.DeleteDashboardRequest{ - UUID: dashboardUUID, - ProfileID: profileID, + UUID: testDashboardUUID, }, - err: types.ErrPermissionDenied, }, }, { - name: "err_repo_random", + name: "err_svc", req: &dashboards.DeleteRequest{ - Uuid: dashboardUUID, + Uuid: testDashboardUUID, }, wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.DeleteDashboardRequest{ - UUID: dashboardUUID, - ProfileID: profileID, + UUID: testDashboardUUID, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newTestData(t) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().Delete(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.err).Times(1) - } - - ctx := context.Background() - if !tt.noUser { - ctx = context.WithValue(ctx, types.UserKey{}, userName) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + DeleteDashboard(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.err). + Times(1) } - got, err := api.Delete(ctx, tt.req) + got, err := api.Delete(context.Background(), tt.req) require.Equal(t, tt.wantCode, status.Code(err)) if tt.wantCode != codes.OK { diff --git a/internal/api/dashboards/v1/grpc/get_all.go b/internal/api/dashboards/v1/grpc/get_all.go index 57ae765..1ee7941 100644 --- a/internal/api/dashboards/v1/grpc/get_all.go +++ b/internal/api/dashboards/v1/grpc/get_all.go @@ -26,15 +26,11 @@ func (a *API) GetAll(ctx context.Context, req *dashboards.GetAllRequest) (*dashb }, ) - // check auth and create profile if its doesn't exist - if _, err := a.profiles.GeIDFromContext(ctx); err != nil { - return nil, grpcutil.ProcessError(err) - } - request := types.GetAllDashboardsRequest{ Limit: int(req.Limit), Offset: int(req.Offset), } + dis, err := a.service.GetAllDashboards(ctx, request) if err != nil { return nil, grpcutil.ProcessError(err) diff --git a/internal/api/dashboards/v1/grpc/get_all_test.go b/internal/api/dashboards/v1/grpc/get_all_test.go index 46e3f18..7878255 100644 --- a/internal/api/dashboards/v1/grpc/get_all_test.go +++ b/internal/api/dashboards/v1/grpc/get_all_test.go @@ -2,7 +2,6 @@ package grpc import ( "context" - "errors" "testing" "github.com/stretchr/testify/require" @@ -15,9 +14,6 @@ import ( ) func TestGetAll(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - type mockArgs struct { req types.GetAllDashboardsRequest resp types.DashboardInfosWithOwner @@ -32,13 +28,12 @@ func TestGetAll(t *testing.T) { wantCode codes.Code mockArgs *mockArgs - noUser bool }{ { - name: "success", + name: "ok", req: &dashboards.GetAllRequest{ - Limit: 2, - Offset: 0, + Limit: int32(testLimit), + Offset: int32(testOffset), }, want: &dashboards.GetAllResponse{ Dashboards: []*dashboards.GetAllResponse_Dashboard{ @@ -49,8 +44,8 @@ func TestGetAll(t *testing.T) { wantCode: codes.OK, mockArgs: &mockArgs{ req: types.GetAllDashboardsRequest{ - Limit: 2, - Offset: 0, + Limit: testLimit, + Offset: testOffset, }, resp: types.DashboardInfosWithOwner{ { @@ -71,61 +66,36 @@ func TestGetAll(t *testing.T) { }, }, { - name: "err_no_user", - wantCode: codes.Unauthenticated, - noUser: true, - }, - { - name: "err_svc_invalid_limit", - req: &dashboards.GetAllRequest{ - Limit: 0, - Offset: 0, - }, - wantCode: codes.InvalidArgument, - }, - { - name: "err_svc_invalid_offset", + name: "err_svc", req: &dashboards.GetAllRequest{ - Limit: 2, - Offset: -10, - }, - wantCode: codes.InvalidArgument, - }, - { - name: "err_repo_random", - req: &dashboards.GetAllRequest{ - Limit: 2, - Offset: 0, + Limit: int32(testLimit), + Offset: int32(testOffset), }, wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.GetAllDashboardsRequest{ - Limit: 2, - Offset: 0, + Limit: testLimit, + Offset: testOffset, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newTestData(t) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().GetAll(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - - ctx := context.Background() - if !tt.noUser { - ctx = context.WithValue(ctx, types.UserKey{}, userName) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + GetAllDashboards(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - got, err := api.GetAll(ctx, tt.req) + got, err := api.GetAll(context.Background(), tt.req) require.Equal(t, tt.wantCode, status.Code(err)) if tt.wantCode != codes.OK { diff --git a/internal/api/dashboards/v1/grpc/get_by_uuid.go b/internal/api/dashboards/v1/grpc/get_by_uuid.go index 0db45b1..91df1ab 100644 --- a/internal/api/dashboards/v1/grpc/get_by_uuid.go +++ b/internal/api/dashboards/v1/grpc/get_by_uuid.go @@ -21,11 +21,6 @@ func (a *API) GetByUUID(ctx context.Context, req *dashboards.GetByUUIDRequest) ( }, ) - // check auth and create profile if its doesn't exist - if _, err := a.profiles.GeIDFromContext(ctx); err != nil { - return nil, grpcutil.ProcessError(err) - } - d, err := a.service.GetDashboardByUUID(ctx, req.Uuid) if err != nil { return nil, grpcutil.ProcessError(err) diff --git a/internal/api/dashboards/v1/grpc/get_by_uuid_test.go b/internal/api/dashboards/v1/grpc/get_by_uuid_test.go index a4d4385..585b755 100644 --- a/internal/api/dashboards/v1/grpc/get_by_uuid_test.go +++ b/internal/api/dashboards/v1/grpc/get_by_uuid_test.go @@ -2,7 +2,6 @@ package grpc import ( "context" - "errors" "testing" "github.com/stretchr/testify/require" @@ -15,13 +14,9 @@ import ( ) func TestGetByUUID(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - dashboardUUID := "064dc707-02b8-7000-8201-02a7f396738a" - dashboardName := "my_dashboard" - dashboardMeta := "my_meta" - dashboardOwner := "owner" - + var ( + dashboardOwner = "owner" + ) type mockArgs struct { uuid string resp types.Dashboard @@ -36,82 +31,54 @@ func TestGetByUUID(t *testing.T) { wantCode codes.Code mockArgs *mockArgs - noUser bool }{ { - name: "success", + name: "ok", req: &dashboards.GetByUUIDRequest{ - Uuid: dashboardUUID, + Uuid: testDashboardUUID, }, want: &dashboards.GetByUUIDResponse{ - Name: dashboardName, - Meta: dashboardMeta, + Name: testDashboardName, + Meta: testDashboardMeta, OwnerName: dashboardOwner, }, wantCode: codes.OK, mockArgs: &mockArgs{ - uuid: dashboardUUID, + uuid: testDashboardUUID, resp: types.Dashboard{ - Name: dashboardName, - Meta: dashboardMeta, + Name: testDashboardName, + Meta: testDashboardMeta, OwnerName: dashboardOwner, }, }, }, { - name: "err_no_user", - wantCode: codes.Unauthenticated, - noUser: true, - }, - { - name: "err_svc_invalid_uuid", - req: &dashboards.GetByUUIDRequest{ - Uuid: "invalid-uuid", - }, - wantCode: codes.InvalidArgument, - }, - { - name: "err_repo_not_found", - req: &dashboards.GetByUUIDRequest{ - Uuid: dashboardUUID, - }, - wantCode: codes.NotFound, - mockArgs: &mockArgs{ - uuid: dashboardUUID, - err: types.ErrNotFound, - }, - }, - { - name: "err_repo_random", + name: "err_svc", req: &dashboards.GetByUUIDRequest{ - Uuid: dashboardUUID, + Uuid: testDashboardUUID, }, wantCode: codes.Internal, mockArgs: &mockArgs{ - uuid: dashboardUUID, - err: errors.New("random repo err"), + uuid: testDashboardUUID, + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newTestData(t) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().GetByUUID(gomock.Any(), tt.mockArgs.uuid). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - - ctx := context.Background() - if !tt.noUser { - ctx = context.WithValue(ctx, types.UserKey{}, userName) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + GetDashboardByUUID(gomock.Any(), tt.mockArgs.uuid). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - got, err := api.GetByUUID(ctx, tt.req) + got, err := api.GetByUUID(context.Background(), tt.req) require.Equal(t, tt.wantCode, status.Code(err)) if tt.wantCode != codes.OK { diff --git a/internal/api/dashboards/v1/grpc/get_my.go b/internal/api/dashboards/v1/grpc/get_my.go index d684019..b65f0cb 100644 --- a/internal/api/dashboards/v1/grpc/get_my.go +++ b/internal/api/dashboards/v1/grpc/get_my.go @@ -26,16 +26,11 @@ func (a *API) GetMy(ctx context.Context, req *dashboards.GetMyRequest) (*dashboa }, ) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - return nil, grpcutil.ProcessError(err) - } - request := types.GetUserDashboardsRequest{ - ProfileID: profileID, - Limit: int(req.Limit), - Offset: int(req.Offset), + Limit: int(req.Limit), + Offset: int(req.Offset), } + dis, err := a.service.GetMyDashboards(ctx, request) if err != nil { return nil, grpcutil.ProcessError(err) diff --git a/internal/api/dashboards/v1/grpc/get_my_test.go b/internal/api/dashboards/v1/grpc/get_my_test.go index e0dda27..cffaf3d 100644 --- a/internal/api/dashboards/v1/grpc/get_my_test.go +++ b/internal/api/dashboards/v1/grpc/get_my_test.go @@ -2,7 +2,6 @@ package grpc import ( "context" - "errors" "testing" "github.com/stretchr/testify/require" @@ -15,9 +14,6 @@ import ( ) func TestGetMy(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - type mockArgs struct { req types.GetUserDashboardsRequest resp types.DashboardInfos @@ -32,13 +28,12 @@ func TestGetMy(t *testing.T) { wantCode codes.Code mockArgs *mockArgs - noUser bool }{ { - name: "success", + name: "ok", req: &dashboards.GetMyRequest{ - Limit: 2, - Offset: 0, + Limit: int32(testLimit), + Offset: int32(testOffset), }, want: &dashboards.GetMyResponse{ Dashboards: []*dashboards.GetMyResponse_Dashboard{ @@ -49,9 +44,8 @@ func TestGetMy(t *testing.T) { wantCode: codes.OK, mockArgs: &mockArgs{ req: types.GetUserDashboardsRequest{ - ProfileID: profileID, - Limit: 2, - Offset: 0, + Limit: testLimit, + Offset: testOffset, }, resp: types.DashboardInfos{ {UUID: "064dc707-02b8-7000-8201-02a7f396738a", Name: "dashboard1"}, @@ -60,62 +54,36 @@ func TestGetMy(t *testing.T) { }, }, { - name: "err_no_user", - wantCode: codes.Unauthenticated, - noUser: true, - }, - { - name: "err_svc_invalid_limit", - req: &dashboards.GetMyRequest{ - Limit: 0, - Offset: 0, - }, - wantCode: codes.InvalidArgument, - }, - { - name: "err_svc_invalid_offset", + name: "err_svc", req: &dashboards.GetMyRequest{ - Limit: 2, - Offset: -10, - }, - wantCode: codes.InvalidArgument, - }, - { - name: "err_repo_random", - req: &dashboards.GetMyRequest{ - Limit: 2, - Offset: 0, + Limit: int32(testLimit), + Offset: int32(testOffset), }, wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.GetUserDashboardsRequest{ - ProfileID: profileID, - Limit: 2, - Offset: 0, + Limit: testLimit, + Offset: testOffset, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newTestData(t) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().GetMy(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - - ctx := context.Background() - if !tt.noUser { - ctx = context.WithValue(ctx, types.UserKey{}, userName) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + GetMyDashboards(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - got, err := api.GetMy(ctx, tt.req) + got, err := api.GetMy(context.Background(), tt.req) require.Equal(t, tt.wantCode, status.Code(err)) if tt.wantCode != codes.OK { diff --git a/internal/api/dashboards/v1/grpc/search.go b/internal/api/dashboards/v1/grpc/search.go index 45213fd..a0bb271 100644 --- a/internal/api/dashboards/v1/grpc/search.go +++ b/internal/api/dashboards/v1/grpc/search.go @@ -40,16 +40,12 @@ func (a *API) Search(ctx context.Context, req *dashboards.SearchRequest) (*dashb span.SetAttributes(spanAttributes...) - // check auth and create profile if its doesn't exist - if _, err := a.profiles.GeIDFromContext(ctx); err != nil { - return nil, grpcutil.ProcessError(err) - } - request := types.SearchDashboardsRequest{ Query: req.Query, Limit: int(req.Limit), Offset: int(req.Offset), } + if req.Filter != nil { request.Filter = &types.SearchDashboardsFilter{ OwnerName: req.Filter.OwnerName, diff --git a/internal/api/dashboards/v1/grpc/search_test.go b/internal/api/dashboards/v1/grpc/search_test.go index 4b08c62..2a9f032 100644 --- a/internal/api/dashboards/v1/grpc/search_test.go +++ b/internal/api/dashboards/v1/grpc/search_test.go @@ -2,7 +2,6 @@ package grpc import ( "context" - "errors" "testing" "github.com/stretchr/testify/require" @@ -15,8 +14,9 @@ import ( ) func TestSearch(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 + var ( + userName = "unnamed" + ) type mockArgs struct { req types.SearchDashboardsRequest @@ -32,14 +32,13 @@ func TestSearch(t *testing.T) { wantCode codes.Code mockArgs *mockArgs - noUser bool }{ { - name: "success", + name: "ok", req: &dashboards.SearchRequest{ Query: "test", - Limit: 2, - Offset: 0, + Limit: int32(testLimit), + Offset: int32(testOffset), }, want: &dashboards.SearchResponse{ Dashboards: []*dashboards.SearchResponse_Dashboard{ @@ -51,8 +50,8 @@ func TestSearch(t *testing.T) { mockArgs: &mockArgs{ req: types.SearchDashboardsRequest{ Query: "test", - Limit: 2, - Offset: 0, + Limit: testLimit, + Offset: testOffset, }, resp: types.DashboardInfosWithOwner{ { @@ -73,11 +72,11 @@ func TestSearch(t *testing.T) { }, }, { - name: "success_with_filter", + name: "ok_with_filter", req: &dashboards.SearchRequest{ Query: "test", - Limit: 2, - Offset: 0, + Limit: int32(testLimit), + Offset: int32(testOffset), Filter: &dashboards.SearchRequest_Filter{ OwnerName: &userName, }, @@ -91,8 +90,8 @@ func TestSearch(t *testing.T) { mockArgs: &mockArgs{ req: types.SearchDashboardsRequest{ Query: "test", - Limit: 2, - Offset: 0, + Limit: testLimit, + Offset: testOffset, Filter: &types.SearchDashboardsFilter{ OwnerName: &userName, }, @@ -109,63 +108,38 @@ func TestSearch(t *testing.T) { }, }, { - name: "err_no_user", - wantCode: codes.Unauthenticated, - noUser: true, - }, - { - name: "err_svc_invalid_limit", - req: &dashboards.SearchRequest{ - Limit: 0, - Offset: 0, - }, - wantCode: codes.InvalidArgument, - }, - { - name: "err_svc_invalid_offset", - req: &dashboards.SearchRequest{ - Limit: 2, - Offset: -10, - }, - wantCode: codes.InvalidArgument, - }, - { - name: "err_repo_random", + name: "err_svc", req: &dashboards.SearchRequest{ Query: "test", - Limit: 2, - Offset: 0, + Limit: int32(testLimit), + Offset: int32(testOffset), }, wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.SearchDashboardsRequest{ Query: "test", - Limit: 2, - Offset: 0, + Limit: testLimit, + Offset: testOffset, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newTestData(t) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().Search(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - - ctx := context.Background() - if !tt.noUser { - ctx = context.WithValue(ctx, types.UserKey{}, userName) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + SearchDashboards(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - got, err := api.Search(ctx, tt.req) + got, err := api.Search(context.Background(), tt.req) require.Equal(t, tt.wantCode, status.Code(err)) if tt.wantCode != codes.OK { diff --git a/internal/api/dashboards/v1/grpc/test_data.go b/internal/api/dashboards/v1/grpc/test_data.go index d230b21..111c97c 100644 --- a/internal/api/dashboards/v1/grpc/test_data.go +++ b/internal/api/dashboards/v1/grpc/test_data.go @@ -1,13 +1,26 @@ package grpc import ( + "errors" "testing" - "github.com/ozontech/seq-ui/internal/api/dashboards/v1/test" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + "go.uber.org/mock/gomock" + + mock "github.com/ozontech/seq-ui/internal/pkg/service/dashboards/mock" +) + +// Shared test data. +var ( + errSomethingWrong = errors.New("something happened wrong") + testDashboardUUID = "064dc707-02b8-7000-8201-02a7f396738a" + testDashboardName = "my_dashboard" + testDashboardMeta = "my_meta" + testLimit = 2 + testOffset = 0 ) -func newTestData(t *testing.T) (*API, *repo_mock.MockDashboards) { - mock, s, p := test.NewTestData(t) - return New(s, p), mock +func setupTestAPI(t *testing.T) (*API, *mock.MockService) { + ctrl := gomock.NewController(t) + mockedSvc := mock.NewMockService(ctrl) + return New(mockedSvc), mockedSvc } diff --git a/internal/api/dashboards/v1/grpc/update.go b/internal/api/dashboards/v1/grpc/update.go index f382c4d..3151157 100644 --- a/internal/api/dashboards/v1/grpc/update.go +++ b/internal/api/dashboards/v1/grpc/update.go @@ -26,18 +26,13 @@ func (a *API) Update(ctx context.Context, req *dashboards.UpdateRequest) (*dashb }, ) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - return nil, grpcutil.ProcessError(err) - } - request := types.UpdateDashboardRequest{ - UUID: req.Uuid, - ProfileID: profileID, - Name: req.Name, - Meta: req.Meta, + UUID: req.Uuid, + Name: req.Name, + Meta: req.Meta, } - if err = a.service.UpdateDashboard(ctx, request); err != nil { + + if err := a.service.UpdateDashboard(ctx, request); err != nil { return nil, grpcutil.ProcessError(err) } diff --git a/internal/api/dashboards/v1/grpc/update_test.go b/internal/api/dashboards/v1/grpc/update_test.go index 1fa2e25..578b5ed 100644 --- a/internal/api/dashboards/v1/grpc/update_test.go +++ b/internal/api/dashboards/v1/grpc/update_test.go @@ -2,7 +2,6 @@ package grpc import ( "context" - "errors" "testing" "github.com/stretchr/testify/require" @@ -15,12 +14,6 @@ import ( ) func TestUpdate(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - dashboardUUID := "064dc707-02b8-7000-8201-02a7f396738a" - dashboardName := "dashboard" - dashboardMeta := "meta" - type mockArgs struct { req types.UpdateDashboardRequest err error @@ -34,153 +27,57 @@ func TestUpdate(t *testing.T) { wantCode codes.Code mockArgs *mockArgs - noUser bool }{ { - name: "success_all", + name: "ok", req: &dashboards.UpdateRequest{ - Uuid: dashboardUUID, - Name: &dashboardName, - Meta: &dashboardMeta, + Uuid: testDashboardUUID, + Name: &testDashboardName, + Meta: &testDashboardMeta, }, want: &dashboards.UpdateResponse{}, wantCode: codes.OK, mockArgs: &mockArgs{ req: types.UpdateDashboardRequest{ - ProfileID: profileID, - UUID: dashboardUUID, - Name: &dashboardName, - Meta: &dashboardMeta, - }, - }, - }, - { - name: "success_only_name", - req: &dashboards.UpdateRequest{ - Uuid: dashboardUUID, - Name: &dashboardName, - }, - want: &dashboards.UpdateResponse{}, - wantCode: codes.OK, - mockArgs: &mockArgs{ - req: types.UpdateDashboardRequest{ - ProfileID: profileID, - UUID: dashboardUUID, - Name: &dashboardName, - }, - }, - }, - { - name: "success_only_meta", - req: &dashboards.UpdateRequest{ - Uuid: dashboardUUID, - Meta: &dashboardMeta, - }, - want: &dashboards.UpdateResponse{}, - wantCode: codes.OK, - mockArgs: &mockArgs{ - req: types.UpdateDashboardRequest{ - ProfileID: profileID, - UUID: dashboardUUID, - Meta: &dashboardMeta, - }, - }, - }, - { - name: "err_no_user", - wantCode: codes.Unauthenticated, - noUser: true, - }, - { - name: "err_svc_invalid_uuid", - req: &dashboards.UpdateRequest{ - Uuid: "invalid-uuid", - Name: &dashboardName, - Meta: &dashboardMeta, - }, - wantCode: codes.InvalidArgument, - }, - { - name: "err_svc_empty_request", - req: &dashboards.UpdateRequest{ - Uuid: dashboardUUID, - }, - wantCode: codes.InvalidArgument, - }, - { - name: "err_repo_not_found", - req: &dashboards.UpdateRequest{ - Uuid: dashboardUUID, - Name: &dashboardName, - Meta: &dashboardMeta, - }, - wantCode: codes.NotFound, - mockArgs: &mockArgs{ - req: types.UpdateDashboardRequest{ - ProfileID: profileID, - UUID: dashboardUUID, - Name: &dashboardName, - Meta: &dashboardMeta, - }, - err: types.ErrNotFound, - }, - }, - { - name: "err_repo_permission_denied", - req: &dashboards.UpdateRequest{ - Uuid: dashboardUUID, - Name: &dashboardName, - Meta: &dashboardMeta, - }, - wantCode: codes.PermissionDenied, - mockArgs: &mockArgs{ - req: types.UpdateDashboardRequest{ - ProfileID: profileID, - UUID: dashboardUUID, - Name: &dashboardName, - Meta: &dashboardMeta, + UUID: testDashboardUUID, + Name: &testDashboardName, + Meta: &testDashboardMeta, }, - err: types.ErrPermissionDenied, }, }, { - name: "err_repo_random", + name: "err_svc", req: &dashboards.UpdateRequest{ - Uuid: dashboardUUID, - Name: &dashboardName, - Meta: &dashboardMeta, + Uuid: testDashboardUUID, + Name: &testDashboardName, + Meta: &testDashboardMeta, }, wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.UpdateDashboardRequest{ - ProfileID: profileID, - UUID: dashboardUUID, - Name: &dashboardName, - Meta: &dashboardMeta, + UUID: testDashboardUUID, + Name: &testDashboardName, + Meta: &testDashboardMeta, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newTestData(t) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().Update(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.err).Times(1) - } - - ctx := context.Background() - if !tt.noUser { - ctx = context.WithValue(ctx, types.UserKey{}, userName) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + UpdateDashboard(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.err). + Times(1) } - got, err := api.Update(ctx, tt.req) + got, err := api.Update(context.Background(), tt.req) require.Equal(t, tt.wantCode, status.Code(err)) if tt.wantCode != codes.OK { diff --git a/internal/api/dashboards/v1/http/api.go b/internal/api/dashboards/v1/http/api.go index b192e69..da629d0 100644 --- a/internal/api/dashboards/v1/http/api.go +++ b/internal/api/dashboards/v1/http/api.go @@ -3,20 +3,17 @@ package http import ( "github.com/go-chi/chi/v5" - "github.com/ozontech/seq-ui/internal/api/profiles" "github.com/ozontech/seq-ui/internal/app/types" - "github.com/ozontech/seq-ui/internal/pkg/service" + "github.com/ozontech/seq-ui/internal/pkg/service/dashboards" ) type API struct { - service service.Service - profiles *profiles.Profiles + service dashboards.Service } -func New(svc service.Service, p *profiles.Profiles) *API { +func New(svc dashboards.Service) *API { return &API{ - service: svc, - profiles: p, + service: svc, } } diff --git a/internal/api/dashboards/v1/http/create.go b/internal/api/dashboards/v1/http/create.go index 70f7179..fef7a33 100644 --- a/internal/api/dashboards/v1/http/create.go +++ b/internal/api/dashboards/v1/http/create.go @@ -40,17 +40,11 @@ func (a *API) serveCreate(w http.ResponseWriter, r *http.Request) { }, ) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - httputil.ProcessError(wr, err) - return - } - req := types.CreateDashboardRequest{ - ProfileID: profileID, - Name: httpReq.Name, - Meta: httpReq.Meta, + Name: httpReq.Name, + Meta: httpReq.Meta, } + uuid, err := a.service.CreateDashboard(ctx, req) if err != nil { httputil.ProcessError(wr, err) diff --git a/internal/api/dashboards/v1/http/create_test.go b/internal/api/dashboards/v1/http/create_test.go index 1582042..ae791f3 100644 --- a/internal/api/dashboards/v1/http/create_test.go +++ b/internal/api/dashboards/v1/http/create_test.go @@ -1,12 +1,7 @@ package http import ( - "context" - "errors" - "fmt" "net/http" - "net/http/httptest" - "strings" "testing" "go.uber.org/mock/gomock" @@ -16,15 +11,11 @@ import ( ) func TestServeCreate(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - dashboardUUID := "064dc707-02b8-7000-8201-02a7f396738a" - dashboardName := "my_dashboard" - dashboardMeta := "my_meta" - - formatReqBody := func(name, meta string) string { - return fmt.Sprintf(`{"name":%q,"meta":%q}`, name, meta) - } + var ( + dashboardUUID = "064dc707-02b8-7000-8201-02a7f396738a" + dashboardMeta = "my_meta" + dashboardName = "my_dashboard" + ) type mockArgs struct { req types.CreateDashboardRequest @@ -35,85 +26,58 @@ func TestServeCreate(t *testing.T) { tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req createRequest + want createResponse + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success", - reqBody: formatReqBody(dashboardName, dashboardMeta), - wantRespBody: fmt.Sprintf(`{"uuid":%q}`, dashboardUUID), - wantStatus: http.StatusOK, + name: "ok", + req: createRequest{Name: dashboardName, Meta: dashboardMeta}, + want: createResponse{UUID: dashboardUUID}, mockArgs: &mockArgs{ req: types.CreateDashboardRequest{ - ProfileID: profileID, - Name: dashboardName, - Meta: dashboardMeta, + Name: dashboardName, + Meta: dashboardMeta, }, resp: dashboardUUID, }, }, { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, - noUser: true, - }, - { - name: "err_no_user", - reqBody: formatReqBody(dashboardName, dashboardMeta), - wantStatus: http.StatusUnauthorized, - noUser: true, - }, - { - name: "err_svc_empty_name", - reqBody: formatReqBody("", dashboardMeta), - wantStatus: http.StatusBadRequest, - }, - { - name: "err_svc_empty_meta", - reqBody: formatReqBody(dashboardName, ""), - wantStatus: http.StatusBadRequest, - }, - { - name: "err_repo_random", - reqBody: formatReqBody(dashboardName, dashboardMeta), - wantStatus: http.StatusInternalServerError, + name: "err_svc", + req: createRequest{Name: dashboardName, Meta: dashboardMeta}, + wantErr: true, mockArgs: &mockArgs{ req: types.CreateDashboardRequest{ - ProfileID: profileID, - Name: dashboardName, - Meta: dashboardMeta, + Name: dashboardName, + Meta: dashboardMeta, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newTestData(t) - req := httptest.NewRequest(http.MethodPost, "/dashboards/v1/", strings.NewReader(tt.reqBody)) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().Create(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - if !tt.noUser { - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, userName)) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + CreateDashboard(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveCreate, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[createRequest, createResponse]{ + Method: http.MethodPost, + Target: "/dashboards/v1/", + Req: tt.req, + Handler: api.serveCreate, + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/dashboards/v1/http/delete.go b/internal/api/dashboards/v1/http/delete.go index d7cfcc4..8a2c958 100644 --- a/internal/api/dashboards/v1/http/delete.go +++ b/internal/api/dashboards/v1/http/delete.go @@ -35,17 +35,11 @@ func (a *API) serveDelete(w http.ResponseWriter, r *http.Request) { }, ) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - httputil.ProcessError(wr, err) - return - } - req := types.DeleteDashboardRequest{ - UUID: uuid, - ProfileID: profileID, + UUID: uuid, } - err = a.service.DeleteDashboard(ctx, req) + + err := a.service.DeleteDashboard(ctx, req) if err != nil { httputil.ProcessError(wr, err) return diff --git a/internal/api/dashboards/v1/http/delete_test.go b/internal/api/dashboards/v1/http/delete_test.go index 506e112..36b4df1 100644 --- a/internal/api/dashboards/v1/http/delete_test.go +++ b/internal/api/dashboards/v1/http/delete_test.go @@ -1,14 +1,10 @@ package http import ( - "context" - "errors" "fmt" "net/http" - "net/http/httptest" "testing" - "github.com/go-chi/chi/v5" "go.uber.org/mock/gomock" "github.com/ozontech/seq-ui/internal/api/httputil" @@ -16,8 +12,6 @@ import ( ) func TestServeDelete(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 dashboardUUID := "064dc707-02b8-7000-8201-02a7f396738a" type mockArgs struct { @@ -28,82 +22,48 @@ func TestServeDelete(t *testing.T) { tests := []struct { name string - uuid string - wantStatus int - + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success", - uuid: dashboardUUID, - wantStatus: http.StatusOK, + name: "ok", mockArgs: &mockArgs{ req: types.DeleteDashboardRequest{ - UUID: dashboardUUID, - ProfileID: profileID, + UUID: dashboardUUID, }, }, }, { - name: "err_no_user", - wantStatus: http.StatusUnauthorized, - noUser: true, - }, - { - name: "err_svc_invalid_uuid", - uuid: "invalid-uuid", - wantStatus: http.StatusBadRequest, - }, - { - name: "err_repo_permission_denied", - uuid: dashboardUUID, - wantStatus: http.StatusForbidden, + name: "err_svc", + wantErr: true, mockArgs: &mockArgs{ req: types.DeleteDashboardRequest{ - UUID: dashboardUUID, - ProfileID: profileID, + UUID: dashboardUUID, }, - err: types.ErrPermissionDenied, - }, - }, - { - name: "err_repo_random", - uuid: dashboardUUID, - wantStatus: http.StatusInternalServerError, - mockArgs: &mockArgs{ - req: types.DeleteDashboardRequest{ - UUID: dashboardUUID, - ProfileID: profileID, - }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newTestData(t) - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/dashboards/v1/%s", tt.uuid), http.NoBody) - rCtx := chi.NewRouteContext() - rCtx.URLParams.Add("uuid", tt.uuid) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rCtx)) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().Delete(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.err).Times(1) - } - if !tt.noUser { - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, userName)) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + DeleteDashboard(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.err). + Times(1) } - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveDelete, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, struct{}]{ + Method: http.MethodDelete, + Target: fmt.Sprintf("/dashboards/v1/%s", dashboardUUID), + Handler: withUUID(api.serveDelete, dashboardUUID), + NoResp: true, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/dashboards/v1/http/get_all.go b/internal/api/dashboards/v1/http/get_all.go index 6380216..49e3023 100644 --- a/internal/api/dashboards/v1/http/get_all.go +++ b/internal/api/dashboards/v1/http/get_all.go @@ -44,16 +44,11 @@ func (a *API) serveGetAll(w http.ResponseWriter, r *http.Request) { }, ) - // check auth and create profile if its doesn't exist - if _, err := a.profiles.GeIDFromContext(ctx); err != nil { - httputil.ProcessError(wr, err) - return - } - req := types.GetAllDashboardsRequest{ Limit: httpReq.Limit, Offset: httpReq.Offset, } + dis, err := a.service.GetAllDashboards(ctx, req) if err != nil { httputil.ProcessError(wr, err) diff --git a/internal/api/dashboards/v1/http/get_all_test.go b/internal/api/dashboards/v1/http/get_all_test.go index f50fdd6..94c3b6a 100644 --- a/internal/api/dashboards/v1/http/get_all_test.go +++ b/internal/api/dashboards/v1/http/get_all_test.go @@ -1,12 +1,7 @@ package http import ( - "context" - "errors" - "fmt" "net/http" - "net/http/httptest" - "strings" "testing" "go.uber.org/mock/gomock" @@ -16,15 +11,6 @@ import ( ) func TestServeGetAll(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - limit := 2 - offset := 0 - - formatReqBody := func(limit, offset int) string { - return fmt.Sprintf(`{"limit":%d,"offset":%d}`, limit, offset) - } - type mockArgs struct { req types.GetAllDashboardsRequest resp types.DashboardInfosWithOwner @@ -34,22 +20,25 @@ func TestServeGetAll(t *testing.T) { tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req getAllRequest + want getAllResponse + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success", - reqBody: formatReqBody(limit, offset), - wantRespBody: `{"dashboards":[{"uuid":"064dc707-02b8-7000-8201-02a7f396738a","name":"dashboard1","owner_name":"user1"},{"uuid":"064dc707-12b9-7000-a238-682b044c908b","name":"dashboard2","owner_name":"user2"}]}`, - wantStatus: http.StatusOK, + name: "ok", + req: getAllRequest{Limit: testLimit, Offset: testOffset}, + want: getAllResponse{ + Dashboards: infosWithOwner{ + {info: info{UUID: "064dc707-02b8-7000-8201-02a7f396738a", Name: "dashboard1"}, OwnerName: "user1"}, + {info: info{UUID: "064dc707-12b9-7000-a238-682b044c908b", Name: "dashboard2"}, OwnerName: "user2"}, + }, + }, mockArgs: &mockArgs{ req: types.GetAllDashboardsRequest{ - Limit: limit, - Offset: offset, + Limit: testLimit, + Offset: testOffset, }, resp: types.DashboardInfosWithOwner{ { @@ -70,62 +59,39 @@ func TestServeGetAll(t *testing.T) { }, }, { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, - noUser: true, - }, - { - name: "err_no_user", - reqBody: formatReqBody(limit, offset), - wantStatus: http.StatusUnauthorized, - noUser: true, - }, - { - name: "err_svc_invalid_limit", - reqBody: formatReqBody(0, offset), - wantStatus: http.StatusBadRequest, - }, - { - name: "err_svc_invalid_offset", - reqBody: formatReqBody(limit, -10), - wantStatus: http.StatusBadRequest, - }, - { - name: "err_repo_random", - reqBody: formatReqBody(limit, offset), - wantStatus: http.StatusInternalServerError, + name: "err_svc", + req: getAllRequest{Limit: testLimit, Offset: testOffset}, + wantErr: true, mockArgs: &mockArgs{ req: types.GetAllDashboardsRequest{ - Limit: limit, - Offset: offset, + Limit: testLimit, + Offset: testOffset, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - req := httptest.NewRequest(http.MethodPost, "/dashboards/v1/all", strings.NewReader(tt.reqBody)) - api, mockedRepo := newTestData(t) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().GetAll(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - if !tt.noUser { - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, userName)) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + GetAllDashboards(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetAll, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[getAllRequest, getAllResponse]{ + Method: http.MethodPost, + Target: "/dashboards/v1/all", + Req: tt.req, + Handler: api.serveGetAll, + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/dashboards/v1/http/get_by_uuid.go b/internal/api/dashboards/v1/http/get_by_uuid.go index 09bad62..d6a8e23 100644 --- a/internal/api/dashboards/v1/http/get_by_uuid.go +++ b/internal/api/dashboards/v1/http/get_by_uuid.go @@ -32,12 +32,6 @@ func (a *API) serveGetByUUID(w http.ResponseWriter, r *http.Request) { Value: attribute.StringValue(uuid), }) - // check auth and create profile if its doesn't exist - if _, err := a.profiles.GeIDFromContext(ctx); err != nil { - httputil.ProcessError(wr, err) - return - } - d, err := a.service.GetDashboardByUUID(ctx, uuid) if err != nil { httputil.ProcessError(wr, err) diff --git a/internal/api/dashboards/v1/http/get_by_uuid_test.go b/internal/api/dashboards/v1/http/get_by_uuid_test.go index e17b483..f3844fe 100644 --- a/internal/api/dashboards/v1/http/get_by_uuid_test.go +++ b/internal/api/dashboards/v1/http/get_by_uuid_test.go @@ -1,14 +1,10 @@ package http import ( - "context" - "errors" "fmt" "net/http" - "net/http/httptest" "testing" - "github.com/go-chi/chi/v5" "go.uber.org/mock/gomock" "github.com/ozontech/seq-ui/internal/api/httputil" @@ -16,8 +12,6 @@ import ( ) func TestServeGetByUUID(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 dashboardUUID := "064dc707-02b8-7000-8201-02a7f396738a" type mockArgs struct { @@ -29,18 +23,18 @@ func TestServeGetByUUID(t *testing.T) { tests := []struct { name string - uuid string - wantRespBody string - wantStatus int + want dashboard + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success", - uuid: dashboardUUID, - wantRespBody: `{"name":"dashboard1","meta":"meta1","owner_name":"owner"}`, - wantStatus: http.StatusOK, + name: "ok", + want: dashboard{ + Name: "dashboard1", + Meta: "meta1", + OwnerName: "owner", + }, mockArgs: &mockArgs{ uuid: dashboardUUID, resp: types.Dashboard{ @@ -51,59 +45,34 @@ func TestServeGetByUUID(t *testing.T) { }, }, { - name: "err_no_user", - wantStatus: http.StatusUnauthorized, - noUser: true, - }, - { - name: "err_svc_invalid_uuid", - uuid: "invalid-uuid", - wantStatus: http.StatusBadRequest, - }, - { - name: "err_repo_not_found", - uuid: dashboardUUID, - wantStatus: http.StatusNotFound, - mockArgs: &mockArgs{ - uuid: dashboardUUID, - err: types.ErrNotFound, - }, - }, - { - name: "err_repo_random", - uuid: dashboardUUID, - wantStatus: http.StatusInternalServerError, + name: "err_svc", + wantErr: true, mockArgs: &mockArgs{ uuid: dashboardUUID, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newTestData(t) - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/dashboards/v1/%s", tt.uuid), http.NoBody) - rCtx := chi.NewRouteContext() - rCtx.URLParams.Add("uuid", tt.uuid) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rCtx)) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().GetByUUID(gomock.Any(), tt.mockArgs.uuid). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - if !tt.noUser { - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, userName)) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + GetDashboardByUUID(gomock.Any(), tt.mockArgs.uuid). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetByUUID, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, dashboard]{ + Method: http.MethodGet, + Target: fmt.Sprintf("/dashboards/v1/%s", dashboardUUID), + Handler: withUUID(api.serveGetByUUID, dashboardUUID), + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/dashboards/v1/http/get_my.go b/internal/api/dashboards/v1/http/get_my.go index 66c182f..661e386 100644 --- a/internal/api/dashboards/v1/http/get_my.go +++ b/internal/api/dashboards/v1/http/get_my.go @@ -44,17 +44,11 @@ func (a *API) serveGetMy(w http.ResponseWriter, r *http.Request) { }, ) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - httputil.ProcessError(wr, err) - return - } - req := types.GetUserDashboardsRequest{ - ProfileID: profileID, - Limit: httpReq.Limit, - Offset: httpReq.Offset, + Limit: httpReq.Limit, + Offset: httpReq.Offset, } + dis, err := a.service.GetMyDashboards(ctx, req) if err != nil { httputil.ProcessError(wr, err) diff --git a/internal/api/dashboards/v1/http/get_my_test.go b/internal/api/dashboards/v1/http/get_my_test.go index 6965487..e3a0a44 100644 --- a/internal/api/dashboards/v1/http/get_my_test.go +++ b/internal/api/dashboards/v1/http/get_my_test.go @@ -1,12 +1,7 @@ package http import ( - "context" - "errors" - "fmt" "net/http" - "net/http/httptest" - "strings" "testing" "go.uber.org/mock/gomock" @@ -16,15 +11,6 @@ import ( ) func TestServeGetMy(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - limit := 2 - offset := 0 - - formatReqBody := func(limit, offset int) string { - return fmt.Sprintf(`{"limit":%d,"offset":%d}`, limit, offset) - } - type mockArgs struct { req types.GetUserDashboardsRequest resp types.DashboardInfos @@ -34,23 +20,25 @@ func TestServeGetMy(t *testing.T) { tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req getMyRequest + want getMyResponse + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success", - reqBody: formatReqBody(limit, offset), - wantRespBody: `{"dashboards":[{"uuid":"064dc707-02b8-7000-8201-02a7f396738a","name":"dashboard1"},{"uuid":"064dc707-12b9-7000-a238-682b044c908b","name":"dashboard2"}]}`, - wantStatus: http.StatusOK, + name: "ok", + req: getMyRequest{Limit: testLimit, Offset: testOffset}, + want: getMyResponse{ + Dashboards: infos{ + {UUID: "064dc707-02b8-7000-8201-02a7f396738a", Name: "dashboard1"}, + {UUID: "064dc707-12b9-7000-a238-682b044c908b", Name: "dashboard2"}, + }, + }, mockArgs: &mockArgs{ req: types.GetUserDashboardsRequest{ - ProfileID: profileID, - Limit: limit, - Offset: offset, + Limit: testLimit, + Offset: testOffset, }, resp: types.DashboardInfos{ {UUID: "064dc707-02b8-7000-8201-02a7f396738a", Name: "dashboard1"}, @@ -59,63 +47,39 @@ func TestServeGetMy(t *testing.T) { }, }, { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, - noUser: true, - }, - { - name: "err_no_user", - reqBody: formatReqBody(limit, offset), - wantStatus: http.StatusUnauthorized, - noUser: true, - }, - { - name: "err_svc_invalid_limit", - reqBody: formatReqBody(0, offset), - wantStatus: http.StatusBadRequest, - }, - { - name: "err_svc_invalid_offset", - reqBody: formatReqBody(limit, -10), - wantStatus: http.StatusBadRequest, - }, - { - name: "err_repo_random", - reqBody: formatReqBody(limit, offset), - wantStatus: http.StatusInternalServerError, + name: "err_svc", + req: getMyRequest{Limit: testLimit, Offset: testOffset}, + wantErr: true, mockArgs: &mockArgs{ req: types.GetUserDashboardsRequest{ - ProfileID: profileID, - Limit: limit, - Offset: offset, + Limit: testLimit, + Offset: testOffset, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - req := httptest.NewRequest(http.MethodPost, "/dashboards/v1/my", strings.NewReader(tt.reqBody)) - api, mockedRepo := newTestData(t) + api, mockedRepo := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().GetMy(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - if !tt.noUser { - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, userName)) - api.profiles.SetID(userName, profileID) + mockedRepo.EXPECT(). + GetMyDashboards(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetMy, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[getMyRequest, getMyResponse]{ + Method: http.MethodPost, + Target: "/dashboards/v1/my", + Req: tt.req, + Handler: api.serveGetMy, + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/dashboards/v1/http/search.go b/internal/api/dashboards/v1/http/search.go index a297b69..71e3728 100644 --- a/internal/api/dashboards/v1/http/search.go +++ b/internal/api/dashboards/v1/http/search.go @@ -57,12 +57,6 @@ func (a *API) serveSearch(w http.ResponseWriter, r *http.Request) { span.SetAttributes(spanAttributes...) - // check auth and create profile if its doesn't exist - if _, err := a.profiles.GeIDFromContext(ctx); err != nil { - httputil.ProcessError(wr, err) - return - } - req := types.SearchDashboardsRequest{ Query: httpReq.Query, Limit: httpReq.Limit, diff --git a/internal/api/dashboards/v1/http/search_test.go b/internal/api/dashboards/v1/http/search_test.go index a47becb..d972192 100644 --- a/internal/api/dashboards/v1/http/search_test.go +++ b/internal/api/dashboards/v1/http/search_test.go @@ -1,12 +1,7 @@ package http import ( - "context" - "errors" - "fmt" "net/http" - "net/http/httptest" - "strings" "testing" "go.uber.org/mock/gomock" @@ -17,27 +12,7 @@ import ( func TestServeSearch(t *testing.T) { userName := "unnamed" - var profileID int64 = 1 - query := "test" - limit := 2 - offset := 0 - filter := &types.SearchDashboardsFilter{ - OwnerName: &userName, - } - - formatReqBody := func(query string, limit, offset int, filter *types.SearchDashboardsFilter) string { - var sb strings.Builder - sb.WriteString(fmt.Sprintf(`{"query":%q,"limit":%d,"offset":%d`, query, limit, offset)) - if filter != nil { - sb.WriteString(`,"filter":{`) - if filter.OwnerName != nil { - sb.WriteString(fmt.Sprintf(`"owner_name":%q`, *filter.OwnerName)) - } - sb.WriteString("}") - } - sb.WriteString("}") - return sb.String() - } + query := "test-query" type mockArgs struct { req types.SearchDashboardsRequest @@ -48,23 +23,26 @@ func TestServeSearch(t *testing.T) { tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req searchRequest + want searchResponse + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success", - reqBody: formatReqBody(query, limit, offset, nil), - wantRespBody: `{"dashboards":[{"uuid":"064dc707-02b8-7000-8201-02a7f396738a","name":"my test dashboard","owner_name":"user1"},{"uuid":"064dc707-12b9-7000-a238-682b044c908b","name":"tested","owner_name":"user2"}]}`, - wantStatus: http.StatusOK, + name: "ok", + req: searchRequest{Query: query, Limit: testLimit, Offset: testOffset}, + want: searchResponse{ + Dashboards: infosWithOwner{ + {info: info{UUID: "064dc707-02b8-7000-8201-02a7f396738a", Name: "my test dashboard"}, OwnerName: "user1"}, + {info: info{UUID: "064dc707-12b9-7000-a238-682b044c908b", Name: "tested"}, OwnerName: "user2"}, + }, + }, mockArgs: &mockArgs{ req: types.SearchDashboardsRequest{ Query: query, - Limit: limit, - Offset: offset, + Limit: testLimit, + Offset: testOffset, }, resp: types.DashboardInfosWithOwner{ { @@ -85,16 +63,26 @@ func TestServeSearch(t *testing.T) { }, }, { - name: "success_with_filter", - reqBody: formatReqBody(query, limit, offset, filter), - wantRespBody: fmt.Sprintf(`{"dashboards":[{"uuid":"064dc707-02b8-7000-8201-02a7f396738a","name":"my test dashboard","owner_name":%q}]}`, userName), - wantStatus: http.StatusOK, + name: "ok_filter", + req: searchRequest{ + Query: query, + Limit: testLimit, + Offset: testOffset, + Filter: &searchFilter{OwnerName: &userName}, + }, + want: searchResponse{ + Dashboards: infosWithOwner{ + {info: info{UUID: "064dc707-02b8-7000-8201-02a7f396738a", Name: "my test dashboard"}, OwnerName: userName}, + }, + }, mockArgs: &mockArgs{ req: types.SearchDashboardsRequest{ Query: query, - Limit: limit, - Offset: offset, - Filter: filter, + Limit: testLimit, + Offset: testOffset, + Filter: &types.SearchDashboardsFilter{ + OwnerName: &userName, + }, }, resp: types.DashboardInfosWithOwner{ { @@ -108,63 +96,40 @@ func TestServeSearch(t *testing.T) { }, }, { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, - noUser: true, - }, - { - name: "err_no_user", - reqBody: formatReqBody(query, limit, offset, nil), - wantStatus: http.StatusUnauthorized, - noUser: true, - }, - { - name: "err_svc_invalid_limit", - reqBody: formatReqBody(query, 0, offset, nil), - wantStatus: http.StatusBadRequest, - }, - { - name: "err_svc_invalid_offset", - reqBody: formatReqBody(query, limit, -10, nil), - wantStatus: http.StatusBadRequest, - }, - { - name: "err_repo_random", - reqBody: formatReqBody(query, limit, offset, nil), - wantStatus: http.StatusInternalServerError, + name: "err_svc", + req: searchRequest{Query: query, Limit: testLimit, Offset: testOffset}, + wantErr: true, mockArgs: &mockArgs{ req: types.SearchDashboardsRequest{ Query: query, - Limit: limit, - Offset: offset, + Limit: testLimit, + Offset: testOffset, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - req := httptest.NewRequest(http.MethodPost, "/dashboards/v1/search", strings.NewReader(tt.reqBody)) - api, mockedRepo := newTestData(t) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().Search(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - if !tt.noUser { - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, userName)) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + SearchDashboards(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveSearch, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[searchRequest, searchResponse]{ + Method: http.MethodPost, + Target: "/dashboards/v1/search", + Req: tt.req, + Handler: api.serveSearch, + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/dashboards/v1/http/test_data.go b/internal/api/dashboards/v1/http/test_data.go index 4c1df04..dbdc1f0 100644 --- a/internal/api/dashboards/v1/http/test_data.go +++ b/internal/api/dashboards/v1/http/test_data.go @@ -1,13 +1,35 @@ package http import ( + "context" + "errors" + "net/http" "testing" - "github.com/ozontech/seq-ui/internal/api/dashboards/v1/test" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + "github.com/go-chi/chi/v5" + "go.uber.org/mock/gomock" + + mock "github.com/ozontech/seq-ui/internal/pkg/service/dashboards/mock" +) + +// Shared test data. +var ( + errSomethingWrong = errors.New("something happened wrong") + testLimit = 2 + testOffset = 0 ) -func newTestData(t *testing.T) (*API, *repo_mock.MockDashboards) { - mock, s, p := test.NewTestData(t) - return New(s, p), mock +func setupTestAPI(t *testing.T) (*API, *mock.MockService) { + ctrl := gomock.NewController(t) + mockedSvc := mock.NewMockService(ctrl) + return New(mockedSvc), mockedSvc +} + +func withUUID(h http.HandlerFunc, uuid string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rCtx := chi.NewRouteContext() + rCtx.URLParams.Add("uuid", uuid) + r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rCtx)) + h(w, r) + } } diff --git a/internal/api/dashboards/v1/http/update.go b/internal/api/dashboards/v1/http/update.go index 8979ba5..5f23b4d 100644 --- a/internal/api/dashboards/v1/http/update.go +++ b/internal/api/dashboards/v1/http/update.go @@ -48,19 +48,13 @@ func (a *API) serveUpdate(w http.ResponseWriter, r *http.Request) { }, ) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - httputil.ProcessError(wr, err) - return - } - req := types.UpdateDashboardRequest{ - UUID: uuid, - ProfileID: profileID, - Name: httpReq.Name, - Meta: httpReq.Meta, + UUID: uuid, + Name: httpReq.Name, + Meta: httpReq.Meta, } - err = a.service.UpdateDashboard(ctx, req) + + err := a.service.UpdateDashboard(ctx, req) if err != nil { httputil.ProcessError(wr, err) return diff --git a/internal/api/dashboards/v1/http/update_test.go b/internal/api/dashboards/v1/http/update_test.go index b9be7f4..bd1226e 100644 --- a/internal/api/dashboards/v1/http/update_test.go +++ b/internal/api/dashboards/v1/http/update_test.go @@ -1,15 +1,10 @@ package http import ( - "context" - "errors" "fmt" "net/http" - "net/http/httptest" - "strings" "testing" - "github.com/go-chi/chi/v5" "go.uber.org/mock/gomock" "github.com/ozontech/seq-ui/internal/api/httputil" @@ -17,27 +12,11 @@ import ( ) func TestServeUpdate(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - dashboardUUID := "064dc707-02b8-7000-8201-02a7f396738a" - dashboardName := "my_dashboard" - dashboardMeta := "my_meta" - - formatReqBody := func(name, meta string) string { - var sb strings.Builder - sb.WriteString("{") - if name != "" { - sb.WriteString(fmt.Sprintf(`"name":%q`, name)) - } - if meta != "" { - if name != "" { - sb.WriteString(",") - } - sb.WriteString(fmt.Sprintf(`"meta":%q`, meta)) - } - sb.WriteString("}") - return sb.String() - } + var ( + dashboardUUID = "064dc707-02b8-7000-8201-02a7f396738a" + dashboardMeta = "my_meta" + dashboardName = "my_dashboard" + ) type mockArgs struct { req types.UpdateDashboardRequest @@ -47,147 +26,55 @@ func TestServeUpdate(t *testing.T) { tests := []struct { name string - uuid string - reqBody string - wantStatus int + req updateRequest + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success_all", - uuid: dashboardUUID, - reqBody: formatReqBody(dashboardName, dashboardMeta), - wantStatus: http.StatusOK, - mockArgs: &mockArgs{ - req: types.UpdateDashboardRequest{ - ProfileID: profileID, - UUID: dashboardUUID, - Name: &dashboardName, - Meta: &dashboardMeta, - }, - }, - }, - { - name: "success_only_name", - uuid: dashboardUUID, - reqBody: formatReqBody(dashboardName, ""), - wantStatus: http.StatusOK, + name: "ok", + req: updateRequest{Name: &dashboardName, Meta: &dashboardMeta}, mockArgs: &mockArgs{ req: types.UpdateDashboardRequest{ - ProfileID: profileID, - UUID: dashboardUUID, - Name: &dashboardName, + UUID: dashboardUUID, + Name: &dashboardName, + Meta: &dashboardMeta, }, }, }, { - name: "success_only_meta", - uuid: dashboardUUID, - reqBody: formatReqBody("", dashboardMeta), - wantStatus: http.StatusOK, + name: "err_svc", + req: updateRequest{Name: &dashboardName, Meta: &dashboardMeta}, + wantErr: true, mockArgs: &mockArgs{ req: types.UpdateDashboardRequest{ - ProfileID: profileID, - UUID: dashboardUUID, - Meta: &dashboardMeta, + UUID: dashboardUUID, + Name: &dashboardName, + Meta: &dashboardMeta, }, - }, - }, - { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, - noUser: true, - }, - { - name: "err_no_user", - reqBody: formatReqBody(dashboardName, dashboardMeta), - wantStatus: http.StatusUnauthorized, - noUser: true, - }, - { - name: "err_svc_invalid_uuid", - uuid: "invalid-uuid", - reqBody: formatReqBody(dashboardName, dashboardMeta), - wantStatus: http.StatusBadRequest, - }, - { - name: "err_svc_empty_request", - uuid: dashboardUUID, - reqBody: `{}`, - wantStatus: http.StatusBadRequest, - }, - { - name: "err_repo_not_found", - uuid: dashboardUUID, - reqBody: formatReqBody(dashboardName, dashboardMeta), - wantStatus: http.StatusNotFound, - mockArgs: &mockArgs{ - req: types.UpdateDashboardRequest{ - ProfileID: profileID, - UUID: dashboardUUID, - Name: &dashboardName, - Meta: &dashboardMeta, - }, - err: types.ErrNotFound, - }, - }, - { - name: "err_repo_permission_denied", - uuid: dashboardUUID, - reqBody: formatReqBody(dashboardName, dashboardMeta), - wantStatus: http.StatusForbidden, - mockArgs: &mockArgs{ - req: types.UpdateDashboardRequest{ - ProfileID: profileID, - UUID: dashboardUUID, - Name: &dashboardName, - Meta: &dashboardMeta, - }, - err: types.ErrPermissionDenied, - }, - }, - { - name: "err_repo_random", - uuid: dashboardUUID, - reqBody: formatReqBody(dashboardName, dashboardMeta), - wantStatus: http.StatusInternalServerError, - mockArgs: &mockArgs{ - req: types.UpdateDashboardRequest{ - ProfileID: profileID, - UUID: dashboardUUID, - Name: &dashboardName, - Meta: &dashboardMeta, - }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newTestData(t) - req := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/dashboards/v1/%s", tt.uuid), strings.NewReader(tt.reqBody)) - rCtx := chi.NewRouteContext() - rCtx.URLParams.Add("uuid", tt.uuid) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rCtx)) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().Update(gomock.Any(), tt.mockArgs.req). + mockedSvc.EXPECT().UpdateDashboard(gomock.Any(), tt.mockArgs.req). Return(tt.mockArgs.err).Times(1) } - if !tt.noUser { - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, userName)) - api.profiles.SetID(userName, profileID) - } - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveUpdate, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[updateRequest, struct{}]{ + Method: http.MethodPatch, + Target: fmt.Sprintf("/dashboards/v1/%s", dashboardUUID), + Req: tt.req, + Handler: withUUID(api.serveUpdate, dashboardUUID), + NoResp: true, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/dashboards/v1/test/data.go b/internal/api/dashboards/v1/test/data.go deleted file mode 100644 index 5ae9e80..0000000 --- a/internal/api/dashboards/v1/test/data.go +++ /dev/null @@ -1,23 +0,0 @@ -package test - -import ( - "testing" - - "go.uber.org/mock/gomock" - - "github.com/ozontech/seq-ui/internal/api/profiles" - repo "github.com/ozontech/seq-ui/internal/pkg/repository" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository/mock" - "github.com/ozontech/seq-ui/internal/pkg/service" -) - -func NewTestData(t *testing.T) (*repo_mock.MockDashboards, service.Service, *profiles.Profiles) { - ctl := gomock.NewController(t) - mockedRepo := repo_mock.NewMockDashboards(ctl) - r := &repo.Repository{ - Dashboards: mockedRepo, - } - s := service.New(r) - p := profiles.New(s) - return mockedRepo, s, p -} diff --git a/internal/api/massexport/v1/grpc/api.go b/internal/api/massexport/v1/grpc/api.go index fc424cf..bb0b2c5 100644 --- a/internal/api/massexport/v1/grpc/api.go +++ b/internal/api/massexport/v1/grpc/api.go @@ -2,11 +2,11 @@ package grpc import ( "github.com/ozontech/seq-ui/internal/pkg/service/massexport" - massexport_v1 "github.com/ozontech/seq-ui/pkg/massexport/v1" + api "github.com/ozontech/seq-ui/pkg/massexport/v1" ) type API struct { - massexport_v1.UnimplementedMassExportServiceServer + api.UnimplementedMassExportServiceServer exporter massexport.Service } diff --git a/internal/api/seqapi/v1/grpc/aggregation_test.go b/internal/api/seqapi/v1/grpc/aggregation_test.go index d2b40ef..785a824 100644 --- a/internal/api/seqapi/v1/grpc/aggregation_test.go +++ b/internal/api/seqapi/v1/grpc/aggregation_test.go @@ -18,15 +18,14 @@ import ( ) func TestGetAggregation(t *testing.T) { - query := "message:error" - from := time.Now() - to := from.Add(time.Second) - + var ( + query = "message:error" + ) tests := []struct { name string req *seqapi.GetAggregationRequest - resp *seqapi.GetAggregationResponse + want *seqapi.GetAggregationResponse apiErr bool clientErr error @@ -37,14 +36,14 @@ func TestGetAggregation(t *testing.T) { name: "ok_multi_agg", req: &seqapi.GetAggregationRequest{ Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Aggregations: []*seqapi.AggregationQuery{ {Field: "test1"}, {Field: "test2"}, }, }, - resp: &seqapi.GetAggregationResponse{ + want: &seqapi.GetAggregationResponse{ Aggregations: test.MakeAggregations(2, 3, nil), Error: &seqapi.Error{ Code: seqapi.ErrorCode_ERROR_CODE_NO, @@ -76,8 +75,8 @@ func TestGetAggregation(t *testing.T) { name: "err_client", req: &seqapi.GetAggregationRequest{ Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Aggregations: []*seqapi.AggregationQuery{ {Field: "test2"}, }, @@ -90,8 +89,8 @@ func TestGetAggregation(t *testing.T) { clientErr: errors.New("client error"), }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -103,39 +102,40 @@ func TestGetAggregation(t *testing.T) { ctrl := gomock.NewController(t) seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().GetAggregation(gomock.Any(), proto.Clone(tt.req)). - Return(proto.Clone(tt.resp), tt.clientErr).Times(1) + seqDbMock.EXPECT(). + GetAggregation(gomock.Any(), proto.Clone(tt.req)). + Return(proto.Clone(tt.want), tt.clientErr). + Times(1) seqData.Mocks.SeqDB = seqDbMock } - s := initTestAPI(seqData) + api := setupTestAPI(seqData) - resp, err := s.GetAggregation(context.Background(), tt.req) + resp, err := api.GetAggregation(context.Background(), tt.req) if tt.apiErr { require.True(t, err != nil) return } require.Equal(t, tt.clientErr, err) - require.True(t, proto.Equal(resp, tt.resp)) + require.True(t, proto.Equal(resp, tt.want)) }) } } func TestGetAggregationWithNormalization(t *testing.T) { - query := "message:error" - from := time.Now() - to := from.Add(time.Second) - targetBucketRate := "3s" - interval := "2s" - + var ( + query = "message:error" + interval = "2s" + targetBucketRate = "3s" + ) tests := []struct { name string - req *seqapi.GetAggregationRequest - resp *seqapi.GetAggregationResponse - normalized_resp *seqapi.GetAggregationResponse + req *seqapi.GetAggregationRequest + want *seqapi.GetAggregationResponse + wantNormalized *seqapi.GetAggregationResponse apiErr bool clientErr error @@ -146,14 +146,14 @@ func TestGetAggregationWithNormalization(t *testing.T) { name: "ok_count", req: &seqapi.GetAggregationRequest{ Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Aggregations: []*seqapi.AggregationQuery{ {Field: "test1", Func: seqapi.AggFunc_AGG_FUNC_COUNT, Interval: &interval}, {Field: "test2", Func: seqapi.AggFunc_AGG_FUNC_COUNT, Interval: &interval}, }, }, - resp: &seqapi.GetAggregationResponse{ + want: &seqapi.GetAggregationResponse{ Aggregations: test.MakeAggregations(2, 3, &test.MakeAggOpts{ Values: []float64{ 1, @@ -165,7 +165,7 @@ func TestGetAggregationWithNormalization(t *testing.T) { Code: seqapi.ErrorCode_ERROR_CODE_NO, }, }, - normalized_resp: &seqapi.GetAggregationResponse{ + wantNormalized: &seqapi.GetAggregationResponse{ Aggregations: test.MakeAggregations(2, 3, &test.MakeAggOpts{ Values: []float64{ 1, @@ -187,14 +187,14 @@ func TestGetAggregationWithNormalization(t *testing.T) { name: "ok_normalize_count", req: &seqapi.GetAggregationRequest{ Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Aggregations: []*seqapi.AggregationQuery{ {Field: "test1", Func: seqapi.AggFunc_AGG_FUNC_COUNT, Interval: &interval, TargetBucketRate: &targetBucketRate}, {Field: "test2", Func: seqapi.AggFunc_AGG_FUNC_COUNT, Interval: &interval, TargetBucketRate: &targetBucketRate}, }, }, - resp: &seqapi.GetAggregationResponse{ + want: &seqapi.GetAggregationResponse{ Aggregations: test.MakeAggregations(2, 3, &test.MakeAggOpts{ TargetBucketRate: targetBucketRate, Values: []float64{ @@ -207,7 +207,7 @@ func TestGetAggregationWithNormalization(t *testing.T) { Code: seqapi.ErrorCode_ERROR_CODE_NO, }, }, - normalized_resp: &seqapi.GetAggregationResponse{ + wantNormalized: &seqapi.GetAggregationResponse{ Aggregations: test.MakeAggregations(2, 3, &test.MakeAggOpts{ TargetBucketRate: targetBucketRate, Values: []float64{ @@ -227,8 +227,8 @@ func TestGetAggregationWithNormalization(t *testing.T) { }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -241,21 +241,21 @@ func TestGetAggregationWithNormalization(t *testing.T) { seqDbMock := mock_seqdb.NewMockClient(ctrl) seqDbMock.EXPECT().GetAggregation(gomock.Any(), proto.Clone(tt.req)). - Return(proto.Clone(tt.resp), tt.clientErr).Times(1) + Return(proto.Clone(tt.want), tt.clientErr).Times(1) seqData.Mocks.SeqDB = seqDbMock } - s := initTestAPI(seqData) + api := setupTestAPI(seqData) - resp, err := s.GetAggregation(context.Background(), tt.req) + resp, err := api.GetAggregation(context.Background(), tt.req) if tt.apiErr { require.True(t, err != nil) return } require.Equal(t, tt.clientErr, err) - require.True(t, proto.Equal(resp, tt.normalized_resp)) + require.True(t, proto.Equal(resp, tt.wantNormalized)) }) } } diff --git a/internal/api/seqapi/v1/grpc/api.go b/internal/api/seqapi/v1/grpc/api.go index 44d66a1..da9a784 100644 --- a/internal/api/seqapi/v1/grpc/api.go +++ b/internal/api/seqapi/v1/grpc/api.go @@ -10,7 +10,6 @@ import ( "go.uber.org/zap" "google.golang.org/grpc/metadata" - "github.com/ozontech/seq-ui/internal/api/profiles" "github.com/ozontech/seq-ui/internal/app/config" "github.com/ozontech/seq-ui/internal/app/types" "github.com/ozontech/seq-ui/internal/pkg/cache" @@ -39,8 +38,7 @@ type API struct { inmemWithRedisCache cache.Cache redisCache cache.Cache nowFn func() time.Time - asyncSearches *asyncsearches.Service - profiles *profiles.Profiles + asyncSearches asyncsearches.Service envsResponse *seqapi.GetEnvsResponse } @@ -49,8 +47,7 @@ func New( seqDBСlients map[string]seqdb.Client, inmemWithRedisCache cache.Cache, redisCache cache.Cache, - asyncSearches *asyncsearches.Service, - p *profiles.Profiles, + asyncSearches asyncsearches.Service, ) *API { var globalfCache *fieldsCache if cfg.FieldsCacheTTL > 0 { @@ -121,7 +118,6 @@ func New( redisCache: redisCache, nowFn: time.Now, asyncSearches: asyncSearches, - profiles: p, envsResponse: parseEnvs(cfg), } } diff --git a/internal/api/seqapi/v1/grpc/cancel_async_search.go b/internal/api/seqapi/v1/grpc/cancel_async_search.go index e54f926..2506f36 100644 --- a/internal/api/seqapi/v1/grpc/cancel_async_search.go +++ b/internal/api/seqapi/v1/grpc/cancel_async_search.go @@ -14,10 +14,7 @@ import ( "github.com/ozontech/seq-ui/tracing" ) -func (a *API) CancelAsyncSearch( - ctx context.Context, - req *seqapi.CancelAsyncSearchRequest, -) (*seqapi.CancelAsyncSearchResponse, error) { +func (a *API) CancelAsyncSearch(ctx context.Context, req *seqapi.CancelAsyncSearchRequest) (*seqapi.CancelAsyncSearchResponse, error) { if a.asyncSearches == nil { return nil, status.Error(codes.Unimplemented, types.ErrAsyncSearchesDisabled.Error()) } @@ -36,12 +33,7 @@ func (a *API) CancelAsyncSearch( }, ) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - return nil, grpcutil.ProcessError(err) - } - - resp, err := a.asyncSearches.CancelAsyncSearch(ctx, profileID, req) + resp, err := a.asyncSearches.CancelAsyncSearch(ctx, req) if err != nil { return nil, grpcutil.ProcessError(err) } diff --git a/internal/api/seqapi/v1/grpc/cancel_async_search_test.go b/internal/api/seqapi/v1/grpc/cancel_async_search_test.go index 610cd92..293fb1b 100644 --- a/internal/api/seqapi/v1/grpc/cancel_async_search_test.go +++ b/internal/api/seqapi/v1/grpc/cancel_async_search_test.go @@ -8,152 +8,98 @@ import ( "go.uber.org/mock/gomock" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" "github.com/ozontech/seq-ui/internal/app/types" - mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" - mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) -func TestServeCancelAsyncSearch(t *testing.T) { - const ( - mockSearchID1 = "69e4a4a6-0922-43bd-952d-060a86c2b622" - mockUserName1 = "some_user_1" - mockUserName2 = "some_user_2" - mockProfileID1 = 1 - mockProfileID2 = 2 - ) - +func TestCancelAsyncSearch(t *testing.T) { type mockArgs struct { - userName string - - profilesReq *types.GetOrCreateUserProfileRequest - profilesResp *types.UserProfile - profilesErr error - - repoResp *types.AsyncSearchInfo - repoErr error + req *seqapi.CancelAsyncSearchRequest + resp *seqapi.CancelAsyncSearchResponse + err error } tests := []struct { name string - req *seqapi.CancelAsyncSearchRequest - resp *seqapi.CancelAsyncSearchResponse - err error + req *seqapi.CancelAsyncSearchRequest + want *seqapi.CancelAsyncSearchResponse + wantCode codes.Code mockArgs *mockArgs }{ { name: "ok", req: &seqapi.CancelAsyncSearchRequest{ - SearchId: mockSearchID1, + SearchId: testSearchID, }, - resp: &seqapi.CancelAsyncSearchResponse{}, + want: &seqapi.CancelAsyncSearchResponse{}, mockArgs: &mockArgs{ - userName: mockUserName1, - profilesReq: &types.GetOrCreateUserProfileRequest{ - UserName: mockUserName1, - }, - profilesResp: &types.UserProfile{ - ID: mockProfileID1, - UserName: mockUserName1, - }, - repoResp: &types.AsyncSearchInfo{ - SearchID: mockSearchID1, - OwnerID: mockProfileID1, - OwnerName: mockUserName1, + req: &seqapi.CancelAsyncSearchRequest{ + SearchId: testSearchID, }, + resp: &seqapi.CancelAsyncSearchResponse{}, }, }, { - name: "err_permission_denied", + name: "invalid_id", req: &seqapi.CancelAsyncSearchRequest{ - SearchId: mockSearchID1, - }, - mockArgs: &mockArgs{ - userName: mockUserName1, - profilesReq: &types.GetOrCreateUserProfileRequest{ - UserName: mockUserName1, - }, - profilesResp: &types.UserProfile{ - ID: mockProfileID1, - UserName: mockUserName1, - }, - repoResp: &types.AsyncSearchInfo{ - SearchID: mockSearchID1, - OwnerID: mockProfileID2, - OwnerName: mockUserName2, - }, + SearchId: "some_invalid_id", }, - err: status.Error(codes.PermissionDenied, "permission denied: cancel async search"), + wantCode: codes.InvalidArgument, }, { - name: "invalid id", + name: "err_svc", req: &seqapi.CancelAsyncSearchRequest{ - SearchId: "some_invalid_id", + SearchId: testSearchID, }, + wantCode: codes.Internal, mockArgs: &mockArgs{ - userName: mockUserName1, + req: &seqapi.CancelAsyncSearchRequest{ + SearchId: testSearchID, + }, + err: errSomethingWrong, }, - err: status.Error(codes.InvalidArgument, "invalid search_id"), }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() + ctrl := gomock.NewController(t) + svcMock := mock_asyncsearches.NewMockService(ctrl) + seqData := test.APITestData{} + seqData.Mocks.AsyncSearchesSvc = svcMock if tt.mockArgs != nil { - ctrl := gomock.NewController(t) - - if tt.err == nil { - seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().CancelAsyncSearch(gomock.Any(), tt.req). - Return(tt.resp, nil).Times(1) - seqData.Mocks.SeqDB = seqDbMock - } - - if tt.mockArgs.profilesResp != nil { - profilesRepoMock := mock_repo.NewMockUserProfiles(ctrl) - profilesRepoMock.EXPECT().GetOrCreate(gomock.Any(), *tt.mockArgs.profilesReq). - Return(*tt.mockArgs.profilesResp, tt.mockArgs.profilesErr).Times(1) - seqData.Mocks.ProfilesRepo = profilesRepoMock - } - - if tt.mockArgs.repoResp != nil { - asyncSearchesRepoMock := mock_repo.NewMockAsyncSearches(ctrl) - asyncSearchesRepoMock.EXPECT().GetAsyncSearchById(gomock.Any(), tt.req.SearchId). - Return(*tt.mockArgs.repoResp, tt.mockArgs.repoErr).Times(1) - seqData.Mocks.AsyncSearchesRepo = asyncSearchesRepoMock - } + svcMock.EXPECT(). + CancelAsyncSearch(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - api := initTestAPIWithAsyncSearches(seqData) - - ctx := context.Background() - ctx = context.WithValue(ctx, types.UserKey{}, tt.mockArgs.userName) + api := setupTestAPI(seqData) + got, err := api.CancelAsyncSearch(context.Background(), tt.req) - resp, err := api.CancelAsyncSearch(ctx, tt.req) - if tt.err == nil { - require.NoError(t, err) - require.True(t, proto.Equal(tt.resp, resp)) - } else { - require.Error(t, err) - require.Equal(t, tt.err, err) + require.Equal(t, tt.wantCode, status.Code(err)) + if tt.wantCode != codes.OK { + return } + require.Equal(t, tt.want, got) }) } } -func TestServeCancelAsyncSearch_Disabled(t *testing.T) { +func TestCancelAsyncSearch_Disabled(t *testing.T) { seqData := test.APITestData{} - api := initTestAPI(seqData) + api := setupTestAPI(seqData) - _, err := api.CancelAsyncSearch(context.Background(), &seqapi.CancelAsyncSearchRequest{}) + _, err := api.CancelAsyncSearch(context.Background(), &seqapi.CancelAsyncSearchRequest{SearchId: testSearchID}) require.Error(t, err) require.Equal(t, status.Error(codes.Unimplemented, types.ErrAsyncSearchesDisabled.Error()), err) } diff --git a/internal/api/seqapi/v1/grpc/cluster_status_test.go b/internal/api/seqapi/v1/grpc/cluster_status_test.go index 8dfb5c3..e7bb0b2 100644 --- a/internal/api/seqapi/v1/grpc/cluster_status_test.go +++ b/internal/api/seqapi/v1/grpc/cluster_status_test.go @@ -2,82 +2,112 @@ package grpc import ( "context" - "errors" "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" - "github.com/ozontech/seq-ui/internal/app/config" mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) func TestStatus(t *testing.T) { - type TestCase struct { - name string - resp *seqapi.StatusResponse - clientErr error + type mockArgs struct { + resp *seqapi.StatusResponse + err error } - someMoment := time.Now() + tests := []struct { + name string - tests := []TestCase{ + want *seqapi.StatusResponse + wantCode codes.Code + + mockArgs *mockArgs + }{ { name: "ok", - resp: &seqapi.StatusResponse{ + want: &seqapi.StatusResponse{ NumberOfStores: 3, - OldestStorageTime: timestamppb.New(someMoment), + OldestStorageTime: timestamppb.New(testTimestamp), Stores: []*seqapi.StoreStatus{ { Host: "host-0", - Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(someMoment)}, + Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(testTimestamp)}, }, { Host: "host-1", - Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(someMoment.Add(1 * time.Hour))}, + Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(testTimestamp.Add(1 * time.Hour))}, }, { Host: "host-2", - Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(someMoment.Add(2 * time.Hour))}, + Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(testTimestamp.Add(2 * time.Hour))}, + }, + }, + }, + mockArgs: &mockArgs{ + resp: &seqapi.StatusResponse{ + NumberOfStores: 3, + OldestStorageTime: timestamppb.New(testTimestamp), + Stores: []*seqapi.StoreStatus{ + { + Host: "host-0", + Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(testTimestamp)}, + }, + { + Host: "host-1", + Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(testTimestamp.Add(1 * time.Hour))}, + }, + { + Host: "host-2", + Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(testTimestamp.Add(2 * time.Hour))}, + }, }, }, }, }, { - name: "err_client", - clientErr: errors.New("client error"), + name: "err_client", + wantCode: codes.Internal, + mockArgs: &mockArgs{ + err: status.Error(codes.Internal, "client error"), + }, }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().Status(gomock.Any(), nil). - Return(proto.Clone(tt.resp), tt.clientErr).Times(1) - cfg := config.SeqAPI{} + seqDbMock.EXPECT(). + Status(gomock.Any(), nil). + Return(proto.Clone(tt.mockArgs.resp), tt.mockArgs.err). + Times(1) seqData := test.APITestData{ - Cfg: cfg, Mocks: test.Mocks{ SeqDB: seqDbMock, }, } - s := initTestAPI(seqData) - resp, err := s.Status(context.Background(), nil) - require.Equal(t, tt.clientErr, err) - require.True(t, proto.Equal(tt.resp, resp)) + api := setupTestAPI(seqData) + + got, err := api.Status(context.Background(), nil) + + require.Equal(t, tt.wantCode, status.Code(err)) + if tt.wantCode != codes.OK { + return + } + require.True(t, proto.Equal(tt.want, got)) }) } } diff --git a/internal/api/seqapi/v1/grpc/delete_async_search.go b/internal/api/seqapi/v1/grpc/delete_async_search.go index 98b3928..4c573ea 100644 --- a/internal/api/seqapi/v1/grpc/delete_async_search.go +++ b/internal/api/seqapi/v1/grpc/delete_async_search.go @@ -14,10 +14,7 @@ import ( "github.com/ozontech/seq-ui/tracing" ) -func (a *API) DeleteAsyncSearch( - ctx context.Context, - req *seqapi.DeleteAsyncSearchRequest, -) (*seqapi.DeleteAsyncSearchResponse, error) { +func (a *API) DeleteAsyncSearch(ctx context.Context, req *seqapi.DeleteAsyncSearchRequest) (*seqapi.DeleteAsyncSearchResponse, error) { if a.asyncSearches == nil { return nil, status.Error(codes.Unimplemented, types.ErrAsyncSearchesDisabled.Error()) } @@ -36,12 +33,7 @@ func (a *API) DeleteAsyncSearch( }, ) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - return nil, grpcutil.ProcessError(err) - } - - resp, err := a.asyncSearches.DeleteAsyncSearch(ctx, profileID, req) + resp, err := a.asyncSearches.DeleteAsyncSearch(ctx, req) if err != nil { return nil, grpcutil.ProcessError(err) } diff --git a/internal/api/seqapi/v1/grpc/delete_async_search_test.go b/internal/api/seqapi/v1/grpc/delete_async_search_test.go index f785ad4..70942eb 100644 --- a/internal/api/seqapi/v1/grpc/delete_async_search_test.go +++ b/internal/api/seqapi/v1/grpc/delete_async_search_test.go @@ -8,163 +8,98 @@ import ( "go.uber.org/mock/gomock" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" "github.com/ozontech/seq-ui/internal/app/types" - mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" - mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) -func TestServeDeleteAsyncSearch(t *testing.T) { - const ( - mockSearchID1 = "69e4a4a6-0922-43bd-952d-060a86c2b622" - mockUserName1 = "some_user_1" - mockUserName2 = "some_user_2" - mockProfileID1 = 1 - mockProfileID2 = 2 - ) - +func TestDeleteAsyncSearch(t *testing.T) { type mockArgs struct { - userName string - - profilesReq *types.GetOrCreateUserProfileRequest - profilesResp *types.UserProfile - profilesErr error - - repoGetAsyncSearchResp *types.AsyncSearchInfo - repoGetAsyncSearchErr error - - repoDeleteAsyncSearchErr error + req *seqapi.DeleteAsyncSearchRequest + resp *seqapi.DeleteAsyncSearchResponse + err error } tests := []struct { name string - req *seqapi.DeleteAsyncSearchRequest - resp *seqapi.DeleteAsyncSearchResponse - err error - - shouldDelete bool + req *seqapi.DeleteAsyncSearchRequest + want *seqapi.DeleteAsyncSearchResponse + wantCode codes.Code mockArgs *mockArgs }{ { name: "ok", req: &seqapi.DeleteAsyncSearchRequest{ - SearchId: mockSearchID1, + SearchId: testSearchID, }, - resp: &seqapi.DeleteAsyncSearchResponse{}, + want: &seqapi.DeleteAsyncSearchResponse{}, mockArgs: &mockArgs{ - userName: mockUserName1, - profilesReq: &types.GetOrCreateUserProfileRequest{ - UserName: mockUserName1, - }, - profilesResp: &types.UserProfile{ - ID: mockProfileID1, - UserName: mockUserName1, - }, - repoGetAsyncSearchResp: &types.AsyncSearchInfo{ - SearchID: mockSearchID1, - OwnerID: mockProfileID1, - OwnerName: mockUserName1, + req: &seqapi.DeleteAsyncSearchRequest{ + SearchId: testSearchID, }, + resp: &seqapi.DeleteAsyncSearchResponse{}, }, - shouldDelete: true, }, { - name: "err_permission_denied", + name: "invalid_id", req: &seqapi.DeleteAsyncSearchRequest{ - SearchId: mockSearchID1, - }, - mockArgs: &mockArgs{ - userName: mockUserName1, - profilesReq: &types.GetOrCreateUserProfileRequest{ - UserName: mockUserName1, - }, - profilesResp: &types.UserProfile{ - ID: mockProfileID1, - UserName: mockUserName1, - }, - repoGetAsyncSearchResp: &types.AsyncSearchInfo{ - SearchID: mockSearchID1, - OwnerID: mockProfileID2, - OwnerName: mockUserName2, - }, + SearchId: "some_invalid_id", }, - err: status.Error(codes.PermissionDenied, "permission denied: delete async search"), + wantCode: codes.InvalidArgument, }, { - name: "invalid id", + name: "err_svc", req: &seqapi.DeleteAsyncSearchRequest{ - SearchId: "some_invalid_id", + SearchId: testSearchID, }, + wantCode: codes.Internal, mockArgs: &mockArgs{ - userName: mockUserName1, + req: &seqapi.DeleteAsyncSearchRequest{ + SearchId: testSearchID, + }, + err: errSomethingWrong, }, - err: status.Error(codes.InvalidArgument, "invalid search_id"), }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() + ctrl := gomock.NewController(t) + svcMock := mock_asyncsearches.NewMockService(ctrl) + seqData := test.APITestData{} + seqData.Mocks.AsyncSearchesSvc = svcMock if tt.mockArgs != nil { - ctrl := gomock.NewController(t) - - if tt.err == nil { - seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().DeleteAsyncSearch(gomock.Any(), tt.req). - Return(tt.resp, nil).Times(1) - seqData.Mocks.SeqDB = seqDbMock - } - - if tt.mockArgs.profilesResp != nil { - profilesRepoMock := mock_repo.NewMockUserProfiles(ctrl) - profilesRepoMock.EXPECT().GetOrCreate(gomock.Any(), *tt.mockArgs.profilesReq). - Return(*tt.mockArgs.profilesResp, tt.mockArgs.profilesErr).Times(1) - seqData.Mocks.ProfilesRepo = profilesRepoMock - } - - if tt.mockArgs.repoGetAsyncSearchResp != nil { - asyncSearchesRepoMock := mock_repo.NewMockAsyncSearches(ctrl) - asyncSearchesRepoMock.EXPECT().GetAsyncSearchById(gomock.Any(), tt.req.SearchId). - Return(*tt.mockArgs.repoGetAsyncSearchResp, tt.mockArgs.repoGetAsyncSearchErr).Times(1) - - if tt.shouldDelete { - asyncSearchesRepoMock.EXPECT().DeleteAsyncSearch(gomock.Any(), tt.req.SearchId). - Return(tt.mockArgs.repoDeleteAsyncSearchErr).Times(1) - } - - seqData.Mocks.AsyncSearchesRepo = asyncSearchesRepoMock - } + svcMock.EXPECT(). + DeleteAsyncSearch(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - api := initTestAPIWithAsyncSearches(seqData) - - ctx := context.Background() - ctx = context.WithValue(ctx, types.UserKey{}, tt.mockArgs.userName) + api := setupTestAPI(seqData) + got, err := api.DeleteAsyncSearch(context.Background(), tt.req) - resp, err := api.DeleteAsyncSearch(ctx, tt.req) - if tt.err == nil { - require.NoError(t, err) - require.True(t, proto.Equal(tt.resp, resp)) - } else { - require.Error(t, err) - require.Equal(t, tt.err, err) + require.Equal(t, tt.wantCode, status.Code(err)) + if tt.wantCode != codes.OK { + return } + require.Equal(t, tt.want, got) }) } } func TestServeDeleteAsyncSearch_Disabled(t *testing.T) { seqData := test.APITestData{} - api := initTestAPI(seqData) + api := setupTestAPI(seqData) - _, err := api.DeleteAsyncSearch(context.Background(), &seqapi.DeleteAsyncSearchRequest{}) + _, err := api.DeleteAsyncSearch(context.Background(), &seqapi.DeleteAsyncSearchRequest{SearchId: testSearchID}) require.Error(t, err) require.Equal(t, status.Error(codes.Unimplemented, types.ErrAsyncSearchesDisabled.Error()), err) } diff --git a/internal/api/seqapi/v1/grpc/events_test.go b/internal/api/seqapi/v1/grpc/events_test.go index 29b116e..3f10c1f 100644 --- a/internal/api/seqapi/v1/grpc/events_test.go +++ b/internal/api/seqapi/v1/grpc/events_test.go @@ -21,18 +21,18 @@ import ( ) func TestGetEvent(t *testing.T) { - eventTime := time.Date(2024, time.December, 31, 10, 20, 30, 400000, time.UTC) - id1 := "test1" - id2 := "test2" - id3 := "test3" - id4 := "test4" - event1 := test.MakeEvent(id1, 1, eventTime) + var ( + id1 = "test1" + id2 = "test2" + id3 = "test3" + id4 = "test4" + ) + event1 := test.MakeEvent(id1, 1, testTimestamp) event1json, _ := proto.Marshal(event1) - event2 := test.MakeEvent(id2, 2, eventTime) + event2 := test.MakeEvent(id2, 2, testTimestamp) event2json, _ := proto.Marshal(event2) event3 := &seqapi.Event{} event3json, _ := proto.Marshal(event3) - err := errors.New("test error") cacheTTL := time.Minute tests := []struct { @@ -55,7 +55,7 @@ func TestGetEvent(t *testing.T) { cacheArgs: test.CacheMockArgs{ Key: id1, Value: string(event1json), - Err: err, + Err: errSomethingWrong, }, }, { @@ -82,7 +82,7 @@ func TestGetEvent(t *testing.T) { cacheArgs: test.CacheMockArgs{ Key: id3, Value: string(event3json), - Err: err, + Err: errSomethingWrong, }, }, { @@ -92,13 +92,13 @@ func TestGetEvent(t *testing.T) { }, cacheArgs: test.CacheMockArgs{ Key: id4, - Err: err, + Err: errSomethingWrong, }, clientErr: errors.New("client error"), }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -112,23 +112,29 @@ func TestGetEvent(t *testing.T) { ctrl := gomock.NewController(t) cacheMock := mock_cache.NewMockCache(ctrl) - cacheMock.EXPECT().Get(gomock.Any(), tt.cacheArgs.Key). - Return(tt.cacheArgs.Value, tt.cacheArgs.Err).Times(1) + cacheMock.EXPECT(). + Get(gomock.Any(), tt.cacheArgs.Key). + Return(tt.cacheArgs.Value, tt.cacheArgs.Err). + Times(1) seqData.Mocks.Cache = cacheMock if tt.cacheArgs.Err != nil { seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().GetEvent(gomock.Any(), proto.Clone(tt.req)). - Return(proto.Clone(tt.resp), tt.clientErr).Times(1) + seqDbMock.EXPECT(). + GetEvent(gomock.Any(), proto.Clone(tt.req)). + Return(proto.Clone(tt.resp), tt.clientErr). + Times(1) seqData.Mocks.SeqDB = seqDbMock if tt.clientErr == nil { - cacheMock.EXPECT().SetWithTTL(gomock.Any(), tt.cacheArgs.Key, tt.cacheArgs.Value, cacheTTL). - Return(nil).Times(1) + cacheMock.EXPECT(). + SetWithTTL(gomock.Any(), tt.cacheArgs.Key, tt.cacheArgs.Value, cacheTTL). + Return(nil). + Times(1) } } - s := initTestAPI(seqData) + s := setupTestAPI(seqData) md := metadata.New(map[string]string{"env": "test"}) ctx := metadata.NewIncomingContext(context.Background(), md) @@ -146,22 +152,21 @@ func TestGetEvent(t *testing.T) { } func TestGetEventWithMasking(t *testing.T) { + var ( + cacheTTL = time.Minute + errCache = errors.New("test error") + ) + type seqDBArgs struct { req *seqapi.GetEventRequest resp *seqapi.GetEventResponse } - eventTime := time.Date(2024, time.December, 31, 10, 20, 30, 400000, time.UTC) - - cacheErr := errors.New("test error") - cacheTTL := time.Minute - tests := []struct { name string shouldMask bool isCached bool - wantErr error maskingCfg *config.Masking }{ @@ -334,7 +339,7 @@ func TestGetEventWithMasking(t *testing.T) { Data: map[string]string{ eventField: eventVal, }, - Time: timestamppb.New(eventTime), + Time: timestamppb.New(testTimestamp), } if shouldMask { event.Data[eventField] = "***" @@ -351,13 +356,11 @@ func TestGetEventWithMasking(t *testing.T) { } eventsData := make([]eventData, 0, len(tests)) - for i := 0; i < len(tests); i++ { + for i := range len(tests) { eventsData = append(eventsData, formEventData(i, tests[i].shouldMask)) } for i, tt := range tests { - i := i - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -372,18 +375,20 @@ func TestGetEventWithMasking(t *testing.T) { }, }, } - ctrl := gomock.NewController(t) + ctrl := gomock.NewController(t) cacheMock := mock_cache.NewMockCache(ctrl) cacheArgs := test.CacheMockArgs{ Key: curEID, Value: string(curEData.eventJson), } if !tt.isCached { - cacheArgs.Err = cacheErr + cacheArgs.Err = errCache } - cacheMock.EXPECT().Get(gomock.Any(), cacheArgs.Key). - Return(cacheArgs.Value, cacheArgs.Err).Times(1) + cacheMock.EXPECT(). + Get(gomock.Any(), cacheArgs.Key). + Return(cacheArgs.Value, cacheArgs.Err). + Times(1) seqData.Mocks.Cache = cacheMock if !tt.isCached { @@ -392,25 +397,23 @@ func TestGetEventWithMasking(t *testing.T) { resp: &seqapi.GetEventResponse{Event: curEData.event}, } seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().GetEvent(gomock.Any(), seqDBArgs.req). - Return(seqDBArgs.resp, nil).Times(1) + seqDbMock.EXPECT(). + GetEvent(gomock.Any(), seqDBArgs.req). + Return(seqDBArgs.resp, nil). + Times(1) seqData.Mocks.SeqDB = seqDbMock - cacheMock.EXPECT().SetWithTTL(gomock.Any(), cacheArgs.Key, cacheArgs.Value, cacheTTL). - Return(nil).Times(1) + cacheMock.EXPECT(). + SetWithTTL(gomock.Any(), cacheArgs.Key, cacheArgs.Value, cacheTTL). + Return(nil). + Times(1) } - s := initTestAPI(seqData) - + api := setupTestAPI(seqData) req := &seqapi.GetEventRequest{Id: curEID} - resp, err := s.GetEvent(context.Background(), req) - - require.Equal(t, tt.wantErr, err) - if tt.wantErr != nil { - return - } - + resp, err := api.GetEvent(context.Background(), req) + require.NoError(t, err) require.True(t, proto.Equal(curEData.wantResp, resp)) }) } diff --git a/internal/api/seqapi/v1/grpc/fetch_async_search_result.go b/internal/api/seqapi/v1/grpc/fetch_async_search_result.go index 1d3d9fa..3ba0fd4 100644 --- a/internal/api/seqapi/v1/grpc/fetch_async_search_result.go +++ b/internal/api/seqapi/v1/grpc/fetch_async_search_result.go @@ -14,10 +14,7 @@ import ( "github.com/ozontech/seq-ui/tracing" ) -func (a *API) FetchAsyncSearchResult( - ctx context.Context, - req *seqapi.FetchAsyncSearchResultRequest, -) (*seqapi.FetchAsyncSearchResultResponse, error) { +func (a *API) FetchAsyncSearchResult(ctx context.Context, req *seqapi.FetchAsyncSearchResultRequest) (*seqapi.FetchAsyncSearchResultResponse, error) { if a.asyncSearches == nil { return nil, status.Error(codes.Unimplemented, types.ErrAsyncSearchesDisabled.Error()) } diff --git a/internal/api/seqapi/v1/grpc/fetch_async_search_result_test.go b/internal/api/seqapi/v1/grpc/fetch_async_search_result_test.go index 48fdb67..fe6ad7d 100644 --- a/internal/api/seqapi/v1/grpc/fetch_async_search_result_test.go +++ b/internal/api/seqapi/v1/grpc/fetch_async_search_result_test.go @@ -9,49 +9,48 @@ import ( "go.uber.org/mock/gomock" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" "github.com/ozontech/seq-ui/internal/app/types" - mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" - mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) func TestServeFetchAsyncSearchResult(t *testing.T) { var ( - mockSearchID = "c9a34cf8-4c66-484e-9cc2-42979d848656" - mockTime = time.Date(2025, 8, 6, 17, 52, 12, 123, time.UTC) - meta = `{"some":"meta"}` + meta = `{"some":"meta"}` ) + type mockArgs struct { + req *seqapi.FetchAsyncSearchResultRequest + err error + } tests := []struct { name string - req *seqapi.FetchAsyncSearchResultRequest - resp *seqapi.FetchAsyncSearchResultResponse - - repoResp types.AsyncSearchInfo + req *seqapi.FetchAsyncSearchResultRequest + want *seqapi.FetchAsyncSearchResultResponse + wantCode codes.Code - err error + mockArgs *mockArgs }{ { name: "ok", req: &seqapi.FetchAsyncSearchResultRequest{ - SearchId: mockSearchID, + SearchId: testSearchID, Limit: 2, Offset: 10, Order: seqapi.Order_ORDER_DESC, }, - resp: &seqapi.FetchAsyncSearchResultResponse{ + want: &seqapi.FetchAsyncSearchResultResponse{ Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, Request: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), Query: "message:error", - From: timestamppb.New(mockTime.Add(-15 * time.Minute)), - To: timestamppb.New(mockTime), + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), Aggs: []*seqapi.AggregationQuery{ { Field: "x", @@ -75,7 +74,7 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { "message": "some error", "x": "2", }, - Time: timestamppb.New(mockTime.Add(-1 * time.Minute)), + Time: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), }, { Id: "017a854298010000-8502fe7f2aa33df3", @@ -84,7 +83,7 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { "message": "some error 2", "x": "8", }, - Time: timestamppb.New(mockTime.Add(-2 * time.Minute)), + Time: timestamppb.New(testTimestamp.Add(-2 * time.Minute)), }, }, Total: 2, @@ -108,14 +107,14 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Value: pointerTo(2), NotExists: 0, Quantiles: []float64{2, 1}, - Ts: timestamppb.New(mockTime), + Ts: timestamppb.New(testTimestamp), }, { Key: "2", Value: pointerTo(8), NotExists: 1, Quantiles: []float64{7, 4}, - Ts: timestamppb.New(mockTime.Add(-1 * time.Minute)), + Ts: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), }, }, NotExists: 2, @@ -126,32 +125,36 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Message: "some error", }, }, - StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, Meta: meta, }, - repoResp: types.AsyncSearchInfo{ - SearchID: mockSearchID, - Meta: meta, + mockArgs: &mockArgs{ + req: &seqapi.FetchAsyncSearchResultRequest{ + SearchId: testSearchID, + Limit: 2, + Offset: 10, + Order: seqapi.Order_ORDER_DESC, + }, }, }, { name: "partial_response", req: &seqapi.FetchAsyncSearchResultRequest{ - SearchId: mockSearchID, + SearchId: testSearchID, Limit: 2, Offset: 10, Order: seqapi.Order_ORDER_DESC, }, - resp: &seqapi.FetchAsyncSearchResultResponse{ + want: &seqapi.FetchAsyncSearchResultResponse{ Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, Request: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), Query: "message:error", - From: timestamppb.New(mockTime.Add(-15 * time.Minute)), - To: timestamppb.New(mockTime), + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), WithDocs: true, Size: 100, }, @@ -164,7 +167,7 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { "message": "some error", "x": "2", }, - Time: timestamppb.New(mockTime.Add(-1 * time.Minute)), + Time: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), }, }, Total: 1, @@ -172,8 +175,8 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Code: seqapi.ErrorCode_ERROR_CODE_NO, }, }, - StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, Meta: meta, @@ -182,60 +185,77 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Message: "partial response", }, }, - repoResp: types.AsyncSearchInfo{ - SearchID: mockSearchID, - Meta: meta, + mockArgs: &mockArgs{ + req: &seqapi.FetchAsyncSearchResultRequest{ + SearchId: testSearchID, + Limit: 2, + Offset: 10, + Order: seqapi.Order_ORDER_DESC, + }, }, }, { - name: "invalid id", + name: "invalid_id", req: &seqapi.FetchAsyncSearchResultRequest{ SearchId: "some_invalid_id", }, - err: status.Error(codes.InvalidArgument, "invalid search_id"), + wantCode: codes.InvalidArgument, + }, + { + name: "err_svc", + req: &seqapi.FetchAsyncSearchResultRequest{ + SearchId: testSearchID, + Limit: 2, + Offset: 10, + Order: seqapi.Order_ORDER_DESC, + }, + wantCode: codes.Internal, + mockArgs: &mockArgs{ + req: &seqapi.FetchAsyncSearchResultRequest{ + SearchId: testSearchID, + Limit: 2, + Offset: 10, + Order: seqapi.Order_ORDER_DESC, + }, + err: errSomethingWrong, + }, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - seqData := test.APITestData{} - ctrl := gomock.NewController(t) + svcMock := mock_asyncsearches.NewMockService(ctrl) - if tt.err == nil { - asyncSearchesRepoMock := mock_repo.NewMockAsyncSearches(ctrl) - asyncSearchesRepoMock.EXPECT().GetAsyncSearchById(gomock.Any(), mockSearchID). - Return(tt.repoResp, nil).Times(1) - seqData.Mocks.AsyncSearchesRepo = asyncSearchesRepoMock + seqData := test.APITestData{} + seqData.Mocks.AsyncSearchesSvc = svcMock - seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().FetchAsyncSearchResult(gomock.Any(), tt.req). - Return(tt.resp, nil).Times(1) - seqData.Mocks.SeqDB = seqDbMock + if tt.mockArgs != nil { + svcMock.EXPECT(). + FetchAsyncSearchResult(gomock.Any(), tt.mockArgs.req). + Return(tt.want, tt.mockArgs.err). + Times(1) } - api := initTestAPIWithAsyncSearches(seqData) - - ctx := context.Background() + api := setupTestAPI(seqData) + got, err := api.FetchAsyncSearchResult(context.Background(), tt.req) - resp, err := api.FetchAsyncSearchResult(ctx, tt.req) - if tt.err == nil { - require.NoError(t, err) - require.True(t, proto.Equal(tt.resp, resp)) - } else { - require.Error(t, err) - require.Equal(t, tt.err, err) + require.Equal(t, tt.wantCode, status.Code(err)) + if tt.wantCode != codes.OK { + return } + require.Equal(t, tt.want, got) }) } } func TestServeFetchAsyncSearchResult_Disabled(t *testing.T) { seqData := test.APITestData{} - api := initTestAPI(seqData) + api := setupTestAPI(seqData) - _, err := api.FetchAsyncSearchResult(context.Background(), &seqapi.FetchAsyncSearchResultRequest{}) + _, err := api.FetchAsyncSearchResult(context.Background(), &seqapi.FetchAsyncSearchResultRequest{SearchId: testSearchID}) require.Error(t, err) require.Equal(t, status.Error(codes.Unimplemented, types.ErrAsyncSearchesDisabled.Error()), err) } diff --git a/internal/api/seqapi/v1/grpc/fields_test.go b/internal/api/seqapi/v1/grpc/fields_test.go index 3bdf539..f52149d 100644 --- a/internal/api/seqapi/v1/grpc/fields_test.go +++ b/internal/api/seqapi/v1/grpc/fields_test.go @@ -103,16 +103,18 @@ func TestGetFields(t *testing.T) { clientErr: errors.New("client error"), }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().GetFields(gomock.Any(), nil). - Return(proto.Clone(tt.seqDBResp), tt.clientErr).Times(1) + + seqDbMock.EXPECT(). + GetFields(gomock.Any(), nil). + Return(proto.Clone(tt.seqDBResp), tt.clientErr). + Times(1) seqData := test.APITestData{ Mocks: test.Mocks{ @@ -123,11 +125,8 @@ func TestGetFields(t *testing.T) { }, } - s := initTestAPI(seqData) - - ctx := context.Background() - - resp, err := s.GetFields(ctx, nil) + api := setupTestAPI(seqData) + resp, err := api.GetFields(context.Background(), nil) require.Equal(t, tt.clientErr, err) require.True(t, proto.Equal(tt.wantResp, resp)) @@ -136,6 +135,9 @@ func TestGetFields(t *testing.T) { } func TestGetFieldsCached(t *testing.T) { + var ( + ttl = 10 * time.Millisecond + ) responses := []*seqapi.GetFieldsResponse{ { Fields: []*seqapi.Field{ @@ -163,12 +165,12 @@ func TestGetFieldsCached(t *testing.T) { seqDbMock := mock_seqdb.NewMockClient(ctrl) for _, r := range responses { - seqDbMock.EXPECT().GetFields(gomock.Any(), nil). - Return(proto.Clone(r), nil).Times(1) + seqDbMock.EXPECT(). + GetFields(gomock.Any(), nil). + Return(proto.Clone(r), nil). + Times(1) } - const ttl = 20 * time.Millisecond - seqData := test.APITestData{ Cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ @@ -179,16 +181,17 @@ func TestGetFieldsCached(t *testing.T) { SeqDB: seqDbMock, }, } - s := initTestAPI(seqData) + + api := setupTestAPI(seqData) for _, r := range responses { - resp, err := s.GetFields(context.Background(), nil) + resp, err := api.GetFields(context.Background(), nil) require.NoError(t, err) require.True(t, proto.Equal(r, resp)) time.Sleep(ttl / 2) - resp, err = s.GetFields(context.Background(), nil) + resp, err = api.GetFields(context.Background(), nil) require.NoError(t, err) require.True(t, proto.Equal(r, resp)) @@ -212,8 +215,8 @@ func TestGetPinnedFields(t *testing.T) { name: "empty", }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -224,9 +227,10 @@ func TestGetPinnedFields(t *testing.T) { }, }, } - s := initTestAPI(seqData) - resp, err := s.GetPinnedFields(context.Background(), nil) + api := setupTestAPI(seqData) + + resp, err := api.GetPinnedFields(context.Background(), nil) require.NoError(t, err) require.Equal(t, len(tt.fields), len(resp.Fields)) diff --git a/internal/api/seqapi/v1/grpc/get_async_searches_list.go b/internal/api/seqapi/v1/grpc/get_async_searches_list.go index f60cb9e..b639207 100644 --- a/internal/api/seqapi/v1/grpc/get_async_searches_list.go +++ b/internal/api/seqapi/v1/grpc/get_async_searches_list.go @@ -13,10 +13,7 @@ import ( "github.com/ozontech/seq-ui/tracing" ) -func (a *API) GetAsyncSearchesList( - ctx context.Context, - req *seqapi.GetAsyncSearchesListRequest, -) (*seqapi.GetAsyncSearchesListResponse, error) { +func (a *API) GetAsyncSearchesList(ctx context.Context, req *seqapi.GetAsyncSearchesListRequest) (*seqapi.GetAsyncSearchesListResponse, error) { if a.asyncSearches == nil { return nil, status.Error(codes.Unimplemented, types.ErrAsyncSearchesDisabled.Error()) } diff --git a/internal/api/seqapi/v1/grpc/get_async_searches_list_test.go b/internal/api/seqapi/v1/grpc/get_async_searches_list_test.go index 9b6c994..88d4237 100644 --- a/internal/api/seqapi/v1/grpc/get_async_searches_list_test.go +++ b/internal/api/seqapi/v1/grpc/get_async_searches_list_test.go @@ -9,65 +9,54 @@ import ( "go.uber.org/mock/gomock" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" "github.com/ozontech/seq-ui/internal/app/types" - mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" - mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) func TestServeGetAsyncSearchesList(t *testing.T) { var ( - mockSearchID1 = "c9a34cf8-4c66-484e-9cc2-42979d848656" - mockSearchID2 = "9e4c068e-d4f4-4a5d-be27-a6524a70d70d" - mockUserName1 = "some_user_1" - mockUserName2 = "some_user_2" - mockProfileID1 int64 = 1 - mockProfileID2 int64 = 1 - errorMsg = "some error" - - mockTime = time.Date(2025, 8, 6, 17, 52, 12, 123, time.UTC) + errorMsg = "some err" + mockUserName1 = "some_user_1" + mockUserName2 = "some_user_2" + mockSearchID2 = "9e4c068e-d4f4-4a5d-be27-a6524a70d70d" ) - type mockArgs struct { - searchIDs []string - - repoReq types.GetAsyncSearchesListRequest - repoResp []types.AsyncSearchInfo - repoErr error + req *seqapi.GetAsyncSearchesListRequest + err error } tests := []struct { name string - req *seqapi.GetAsyncSearchesListRequest - resp *seqapi.GetAsyncSearchesListResponse - err error + req *seqapi.GetAsyncSearchesListRequest + want *seqapi.GetAsyncSearchesListResponse + wantCode codes.Code mockArgs *mockArgs }{ { name: "ok_no_filters", req: &seqapi.GetAsyncSearchesListRequest{}, - resp: &seqapi.GetAsyncSearchesListResponse{ + want: &seqapi.GetAsyncSearchesListResponse{ Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchId: mockSearchID1, + SearchId: testSearchID, Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, Request: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), Query: "message:error", - From: timestamppb.New(mockTime.Add(-15 * time.Minute)), - To: timestamppb.New(mockTime), + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), WithDocs: true, Size: 100, }, - StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, OwnerName: mockUserName1, @@ -79,8 +68,8 @@ func TestServeGetAsyncSearchesList(t *testing.T) { Request: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(360 * time.Second), Query: "message:error and level:3", - From: timestamppb.New(mockTime.Add(-1 * time.Hour)), - To: timestamppb.New(mockTime), + From: timestamppb.New(testTimestamp.Add(-1 * time.Hour)), + To: timestamppb.New(testTimestamp), Aggs: []*seqapi.AggregationQuery{ { Field: "x", @@ -93,9 +82,9 @@ func TestServeGetAsyncSearchesList(t *testing.T) { }, WithDocs: false, }, - StartedAt: timestamppb.New(mockTime.Add(-60 * time.Second)), - ExpiresAt: timestamppb.New(mockTime.Add(300 * time.Second)), - CanceledAt: timestamppb.New(mockTime), + StartedAt: timestamppb.New(testTimestamp.Add(-60 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(300 * time.Second)), + CanceledAt: timestamppb.New(testTimestamp), Progress: 1, DiskUsage: 256, OwnerName: mockUserName2, @@ -103,20 +92,7 @@ func TestServeGetAsyncSearchesList(t *testing.T) { }, }, mockArgs: &mockArgs{ - repoReq: types.GetAsyncSearchesListRequest{}, - repoResp: []types.AsyncSearchInfo{ - { - SearchID: mockSearchID1, - OwnerID: mockProfileID1, - OwnerName: mockUserName1, - }, - { - SearchID: mockSearchID2, - OwnerID: mockProfileID2, - OwnerName: mockUserName2, - }, - }, - searchIDs: []string{mockSearchID1, mockSearchID2}, + req: &seqapi.GetAsyncSearchesListRequest{}, }, }, { @@ -127,21 +103,21 @@ func TestServeGetAsyncSearchesList(t *testing.T) { Limit: 10, Offset: 20, }, - resp: &seqapi.GetAsyncSearchesListResponse{ + want: &seqapi.GetAsyncSearchesListResponse{ Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchId: mockSearchID1, + SearchId: testSearchID, Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, Request: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), Query: "message:error", - From: timestamppb.New(mockTime.Add(-15 * time.Minute)), - To: timestamppb.New(mockTime), + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), WithDocs: true, Size: 100, }, - StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, OwnerName: mockUserName1, @@ -149,37 +125,32 @@ func TestServeGetAsyncSearchesList(t *testing.T) { }, }, mockArgs: &mockArgs{ - repoReq: types.GetAsyncSearchesListRequest{ - Owner: &mockUserName1, - }, - repoResp: []types.AsyncSearchInfo{ - { - SearchID: mockSearchID1, - OwnerID: mockProfileID1, - OwnerName: mockUserName1, - }, + req: &seqapi.GetAsyncSearchesListRequest{ + Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE.Enum(), + OwnerName: &mockUserName1, + Limit: 10, + Offset: 20, }, - searchIDs: []string{mockSearchID1}, }, }, { name: "partial_response", req: &seqapi.GetAsyncSearchesListRequest{}, - resp: &seqapi.GetAsyncSearchesListResponse{ + want: &seqapi.GetAsyncSearchesListResponse{ Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchId: mockSearchID1, + SearchId: testSearchID, Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, Request: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), Query: "message:error", - From: timestamppb.New(mockTime.Add(-15 * time.Minute)), - To: timestamppb.New(mockTime), + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), WithDocs: true, Size: 100, }, - StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, OwnerName: mockUserName1, @@ -191,15 +162,7 @@ func TestServeGetAsyncSearchesList(t *testing.T) { }, }, mockArgs: &mockArgs{ - repoReq: types.GetAsyncSearchesListRequest{}, - repoResp: []types.AsyncSearchInfo{ - { - SearchID: mockSearchID1, - OwnerID: mockProfileID1, - OwnerName: mockUserName1, - }, - }, - searchIDs: []string{mockSearchID1}, + req: &seqapi.GetAsyncSearchesListRequest{}, }, }, { @@ -208,7 +171,7 @@ func TestServeGetAsyncSearchesList(t *testing.T) { Limit: -10, Offset: 10, }, - err: status.Error(codes.InvalidArgument, "invalid request field: 'limit' must be non-negative"), + wantCode: codes.InvalidArgument, }, { name: "err_offset", @@ -216,48 +179,42 @@ func TestServeGetAsyncSearchesList(t *testing.T) { Limit: 10, Offset: -10, }, - err: status.Error(codes.InvalidArgument, "invalid request field: 'offset' must be non-negative"), + wantCode: codes.InvalidArgument, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() + ctrl := gomock.NewController(t) + svcMock := mock_asyncsearches.NewMockService(ctrl) + seqData := test.APITestData{} + seqData.Mocks.AsyncSearchesSvc = svcMock if tt.mockArgs != nil { - ctrl := gomock.NewController(t) - - asyncSearchesRepoMock := mock_repo.NewMockAsyncSearches(ctrl) - asyncSearchesRepoMock.EXPECT().GetAsyncSearchesList(gomock.Any(), tt.mockArgs.repoReq). - Return(tt.mockArgs.repoResp, tt.mockArgs.repoErr).Times(1) - seqData.Mocks.AsyncSearchesRepo = asyncSearchesRepoMock - - seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().GetAsyncSearchesList(gomock.Any(), tt.req, tt.mockArgs.searchIDs). - Return(tt.resp, nil).Times(1) - seqData.Mocks.SeqDB = seqDbMock + svcMock.EXPECT(). + GetAsyncSearchesList(gomock.Any(), tt.mockArgs.req). + Return(tt.want, tt.mockArgs.err). + Times(1) } - api := initTestAPIWithAsyncSearches(seqData) - - ctx := context.Background() + api := setupTestAPI(seqData) + got, err := api.GetAsyncSearchesList(context.Background(), tt.req) - resp, err := api.GetAsyncSearchesList(ctx, tt.req) - if tt.err == nil { - require.NoError(t, err) - require.True(t, proto.Equal(tt.resp, resp)) - } else { - require.Error(t, err) - require.Equal(t, tt.err, err) + require.Equal(t, tt.wantCode, status.Code(err)) + if tt.wantCode != codes.OK { + return } + require.Equal(t, tt.want, got) }) } } func TestServeGetAsyncSearchesList_Disabled(t *testing.T) { seqData := test.APITestData{} - api := initTestAPI(seqData) + api := setupTestAPI(seqData) _, err := api.GetAsyncSearchesList(context.Background(), &seqapi.GetAsyncSearchesListRequest{}) require.Error(t, err) diff --git a/internal/api/seqapi/v1/grpc/get_envs_test.go b/internal/api/seqapi/v1/grpc/get_envs_test.go index 7f8140e..5f5a1a4 100644 --- a/internal/api/seqapi/v1/grpc/get_envs_test.go +++ b/internal/api/seqapi/v1/grpc/get_envs_test.go @@ -141,7 +141,6 @@ func TestGetEnvs(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() api := API{ diff --git a/internal/api/seqapi/v1/grpc/histogram_test.go b/internal/api/seqapi/v1/grpc/histogram_test.go index aa75fda..8b4d25b 100644 --- a/internal/api/seqapi/v1/grpc/histogram_test.go +++ b/internal/api/seqapi/v1/grpc/histogram_test.go @@ -2,7 +2,6 @@ package grpc import ( "context" - "errors" "testing" "time" @@ -17,16 +16,15 @@ import ( ) func TestGetHistogram(t *testing.T) { - query := "message:error" - from := time.Now() - to := from.Add(time.Second) - interval := "5s" - + var ( + query = "message:error" + interval = "2s" + ) tests := []struct { name string req *seqapi.GetHistogramRequest - resp *seqapi.GetHistogramResponse + want *seqapi.GetHistogramResponse clientErr error }{ @@ -34,11 +32,11 @@ func TestGetHistogram(t *testing.T) { name: "ok", req: &seqapi.GetHistogramRequest{ Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Interval: interval, }, - resp: &seqapi.GetHistogramResponse{ + want: &seqapi.GetHistogramResponse{ Histogram: test.MakeHistogram(5), Error: &seqapi.Error{ Code: seqapi.ErrorCode_ERROR_CODE_NO, @@ -50,11 +48,11 @@ func TestGetHistogram(t *testing.T) { req: &seqapi.GetHistogramRequest{ Interval: interval, }, - clientErr: errors.New("client error"), + clientErr: errSomethingWrong, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -63,16 +61,17 @@ func TestGetHistogram(t *testing.T) { ctrl := gomock.NewController(t) seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().GetHistogram(gomock.Any(), proto.Clone(tt.req)). - Return(proto.Clone(tt.resp), tt.clientErr).Times(1) - + seqDbMock.EXPECT(). + GetHistogram(gomock.Any(), proto.Clone(tt.req)). + Return(proto.Clone(tt.want), tt.clientErr). + Times(1) seqData.Mocks.SeqDB = seqDbMock - s := initTestAPI(seqData) - resp, err := s.GetHistogram(context.Background(), tt.req) + api := setupTestAPI(seqData) + resp, err := api.GetHistogram(context.Background(), tt.req) require.Equal(t, tt.clientErr, err) - require.True(t, proto.Equal(tt.resp, resp)) + require.True(t, proto.Equal(tt.want, resp)) }) } } diff --git a/internal/api/seqapi/v1/grpc/limits_test.go b/internal/api/seqapi/v1/grpc/limits_test.go index c361d79..30892c6 100644 --- a/internal/api/seqapi/v1/grpc/limits_test.go +++ b/internal/api/seqapi/v1/grpc/limits_test.go @@ -43,15 +43,15 @@ func TestGetLimits(t *testing.T) { want: &seqapi.GetLimitsResponse{}, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() seqData := test.APITestData{ Cfg: tt.cfg, } - s := initTestAPI(seqData) + s := setupTestAPI(seqData) resp, err := s.GetLimits(context.Background(), nil) require.NoError(t, err) diff --git a/internal/api/seqapi/v1/grpc/logs_lifespan_test.go b/internal/api/seqapi/v1/grpc/logs_lifespan_test.go index 872f927..9b2fc2f 100644 --- a/internal/api/seqapi/v1/grpc/logs_lifespan_test.go +++ b/internal/api/seqapi/v1/grpc/logs_lifespan_test.go @@ -2,7 +2,6 @@ package grpc import ( "context" - "errors" "strconv" "testing" "time" @@ -22,21 +21,17 @@ import ( ) func TestGetLogsLifespan(t *testing.T) { - const ( - cacheKey = "logs_lifespan" - cacheTTL = 1 * time.Minute - - result = 10 * time.Hour + var ( resultStr = "36000" // 10(h) * 60(min/h) * 60(sec/min) + cacheKey = "logs_lifespan" + result = 10 * time.Hour + cacheTTL = time.Minute ) - unparsable := func(s string) bool { _, err := strconv.Atoi(s) return err != nil } - oldestStorageTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) - tests := []struct { name string @@ -66,7 +61,7 @@ func TestGetLogsLifespan(t *testing.T) { Value: resultStr, }, clientResp: &seqapi.StatusResponse{ - OldestStorageTime: timestamppb.New(oldestStorageTime), + OldestStorageTime: timestamppb.New(testTimestamp), }, resp: &seqapi.GetLogsLifespanResponse{ Lifespan: durationpb.New(result), @@ -81,7 +76,7 @@ func TestGetLogsLifespan(t *testing.T) { Value: resultStr, }, clientResp: &seqapi.StatusResponse{ - OldestStorageTime: timestamppb.New(oldestStorageTime), + OldestStorageTime: timestamppb.New(testTimestamp), }, resp: &seqapi.GetLogsLifespanResponse{ Lifespan: durationpb.New(result), @@ -92,7 +87,7 @@ func TestGetLogsLifespan(t *testing.T) { getOp: test.CacheMockArgs{ Err: cache.ErrNotFound, }, - clientErr: errors.New("network error"), + clientErr: errSomethingWrong, }, { name: "err_nil_oldest_storage_time", @@ -104,8 +99,8 @@ func TestGetLogsLifespan(t *testing.T) { }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -117,28 +112,35 @@ func TestGetLogsLifespan(t *testing.T) { }, }, } - ctrl := gomock.NewController(t) + ctrl := gomock.NewController(t) cacheMock := mock_cache.NewMockCache(ctrl) - cacheMock.EXPECT().Get(gomock.Any(), cacheKey). - Return(tt.getOp.Value, tt.getOp.Err).Times(1) + + cacheMock.EXPECT(). + Get(gomock.Any(), cacheKey). + Return(tt.getOp.Value, tt.getOp.Err). + Times(1) seqData.Mocks.Cache = cacheMock if tt.getOp.Err != nil || unparsable(tt.getOp.Value) { seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().Status(gomock.Any(), gomock.Any()). - Return(proto.Clone(tt.clientResp), tt.clientErr).Times(1) + seqDbMock.EXPECT(). + Status(gomock.Any(), gomock.Any()). + Return(proto.Clone(tt.clientResp), tt.clientErr). + Times(1) seqData.Mocks.SeqDB = seqDbMock if tt.clientErr == nil && tt.clientResp.OldestStorageTime != nil { - cacheMock.EXPECT().SetWithTTL(gomock.Any(), cacheKey, tt.setOp.Value, cacheTTL). - Return(tt.setOp.Err).Times(1) + cacheMock.EXPECT(). + SetWithTTL(gomock.Any(), cacheKey, tt.setOp.Value, cacheTTL). + Return(tt.setOp.Err). + Times(1) } } - s := initTestAPI(seqData) + s := setupTestAPI(seqData) s.nowFn = func() time.Time { - return oldestStorageTime.Add(result) + return testTimestamp.Add(result) } resp, err := s.GetLogsLifespan(context.Background(), nil) diff --git a/internal/api/seqapi/v1/grpc/search_test.go b/internal/api/seqapi/v1/grpc/search_test.go index d83d22a..b818531 100644 --- a/internal/api/seqapi/v1/grpc/search_test.go +++ b/internal/api/seqapi/v1/grpc/search_test.go @@ -2,7 +2,6 @@ package grpc import ( "context" - "errors" "testing" "time" @@ -19,13 +18,10 @@ import ( ) func TestSearch(t *testing.T) { - query := "message:error" - from := time.Now() - to := from.Add(time.Second) - var limit int32 = 3 - - eventTime := time.Date(2024, time.December, 31, 10, 20, 30, 400000, time.UTC) - + var ( + query = "message:error" + limit int32 = 3 + ) tests := []struct { name string @@ -42,8 +38,8 @@ func TestSearch(t *testing.T) { name: "ok", req: &seqapi.SearchRequest{ Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: limit, Offset: 0, WithTotal: true, @@ -57,7 +53,7 @@ func TestSearch(t *testing.T) { Order: seqapi.Order_ORDER_ASC, }, resp: &seqapi.SearchResponse{ - Events: test.MakeEvents(int(limit), eventTime), + Events: test.MakeEvents(int(limit), testTimestamp), Total: int64(limit), Histogram: test.MakeHistogram(2), Aggregations: test.MakeAggregations(2, 2, nil), @@ -113,8 +109,8 @@ func TestSearch(t *testing.T) { name: "err_offset_too_high", req: &seqapi.SearchRequest{ Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: limit, Offset: 11, }, @@ -130,18 +126,18 @@ func TestSearch(t *testing.T) { name: "err_total_too_high", req: &seqapi.SearchRequest{ Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: limit, Offset: 0, WithTotal: true, }, resp: &seqapi.SearchResponse{ - Events: test.MakeEvents(int(limit), eventTime), + Events: test.MakeEvents(int(limit), testTimestamp), Total: int64(limit) + 1, }, wantResp: &seqapi.SearchResponse{ - Events: test.MakeEvents(int(limit), eventTime), + Events: test.MakeEvents(int(limit), testTimestamp), Total: int64(limit) + 1, Error: &seqapi.Error{ Code: seqapi.ErrorCode_ERROR_CODE_QUERY_TOO_HEAVY, @@ -159,8 +155,8 @@ func TestSearch(t *testing.T) { name: "err_client", req: &seqapi.SearchRequest{ Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: limit, Offset: 0, }, @@ -169,11 +165,11 @@ func TestSearch(t *testing.T) { MaxSearchLimit: 5, }, }), - clientErr: errors.New("client error"), + clientErr: errSomethingWrong, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -185,14 +181,16 @@ func TestSearch(t *testing.T) { ctrl := gomock.NewController(t) seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().Search(gomock.Any(), proto.Clone(tt.req)). - Return(proto.Clone(tt.resp), tt.clientErr).Times(1) + seqDbMock.EXPECT(). + Search(gomock.Any(), proto.Clone(tt.req)). + Return(proto.Clone(tt.resp), tt.clientErr). + Times(1) seqData.Mocks.SeqDB = seqDbMock } - s := initTestAPI(seqData) - resp, err := s.Search(context.Background(), tt.req) + api := setupTestAPI(seqData) + resp, err := api.Search(context.Background(), tt.req) if tt.apiErr { require.NotNil(t, err) return diff --git a/internal/api/seqapi/v1/grpc/start_async_search.go b/internal/api/seqapi/v1/grpc/start_async_search.go index 4c31659..86641ed 100644 --- a/internal/api/seqapi/v1/grpc/start_async_search.go +++ b/internal/api/seqapi/v1/grpc/start_async_search.go @@ -14,10 +14,7 @@ import ( "github.com/ozontech/seq-ui/tracing" ) -func (a *API) StartAsyncSearch( - ctx context.Context, - req *seqapi.StartAsyncSearchRequest, -) (*seqapi.StartAsyncSearchResponse, error) { +func (a *API) StartAsyncSearch(ctx context.Context, req *seqapi.StartAsyncSearchRequest) (*seqapi.StartAsyncSearchResponse, error) { if a.asyncSearches == nil { return nil, status.Error(codes.Unimplemented, types.ErrAsyncSearchesDisabled.Error()) } @@ -67,12 +64,7 @@ func (a *API) StartAsyncSearch( span.SetAttributes(spanAttributes...) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - return nil, grpcutil.ProcessError(err) - } - - resp, err := a.asyncSearches.StartAsyncSearch(ctx, profileID, req) + resp, err := a.asyncSearches.StartAsyncSearch(ctx, req) if err != nil { return nil, grpcutil.ProcessError(err) } diff --git a/internal/api/seqapi/v1/grpc/start_async_search_test.go b/internal/api/seqapi/v1/grpc/start_async_search_test.go index 14c0dd9..03cbc21 100644 --- a/internal/api/seqapi/v1/grpc/start_async_search_test.go +++ b/internal/api/seqapi/v1/grpc/start_async_search_test.go @@ -9,43 +9,31 @@ import ( "go.uber.org/mock/gomock" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" "github.com/ozontech/seq-ui/internal/app/types" - mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" - mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) func TestServeStartAsyncSearch(t *testing.T) { - const ( - mockSearchID = "c9a34cf8-4c66-484e-9cc2-42979d848656" - mockUserName = "some_user" - mockProfileID = 1 - meta = `{"some":"meta"}` + var ( + query = "message:error" + meta = `{"some":"meta"}` ) - - query := "message:error" - from := time.Date(2023, time.September, 25, 10, 20, 30, 0, time.UTC) - to := from.Add(time.Second) - type mockArgs struct { - profilesReq types.GetOrCreateUserProfileRequest - profilesResp types.UserProfile - profilesErr error - - repoReq types.SaveAsyncSearchRequest - repoErr error + resp *seqapi.StartAsyncSearchResponse + err error } tests := []struct { name string - req *seqapi.StartAsyncSearchRequest - resp *seqapi.StartAsyncSearchResponse + req *seqapi.StartAsyncSearchRequest + want *seqapi.StartAsyncSearchResponse + wantCode codes.Code mockArgs *mockArgs }{ @@ -54,8 +42,8 @@ func TestServeStartAsyncSearch(t *testing.T) { req: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), WithDocs: true, Size: 100, Hist: &seqapi.StartAsyncSearchRequest_HistQuery{ @@ -71,25 +59,44 @@ func TestServeStartAsyncSearch(t *testing.T) { }, Meta: meta, }, - resp: &seqapi.StartAsyncSearchResponse{ - SearchId: mockSearchID, + want: &seqapi.StartAsyncSearchResponse{ + SearchId: testSearchID, }, mockArgs: &mockArgs{ - profilesReq: types.GetOrCreateUserProfileRequest{ - UserName: mockUserName, + resp: &seqapi.StartAsyncSearchResponse{ + SearchId: testSearchID, }, - profilesResp: types.UserProfile{ - ID: mockProfileID, - UserName: mockUserName, + }, + }, + { + name: "err_svc", + req: &seqapi.StartAsyncSearchRequest{ + Retention: durationpb.New(60 * time.Second), + Query: query, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), + WithDocs: true, + Size: 100, + Hist: &seqapi.StartAsyncSearchRequest_HistQuery{ + Interval: "1s", }, - repoReq: types.SaveAsyncSearchRequest{ - SearchID: mockSearchID, - OwnerID: mockProfileID, - Meta: meta, + Aggs: []*seqapi.AggregationQuery{ + { + Field: "v", + GroupBy: "level", + Func: seqapi.AggFunc_AGG_FUNC_AVG, + Quantiles: []float64{0.95}, + }, }, + Meta: meta, + }, + wantCode: codes.Internal, + mockArgs: &mockArgs{ + err: errSomethingWrong, }, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -98,39 +105,31 @@ func TestServeStartAsyncSearch(t *testing.T) { if tt.mockArgs != nil { ctrl := gomock.NewController(t) + svcMock := mock_asyncsearches.NewMockService(ctrl) - seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().StartAsyncSearch(gomock.Any(), tt.req). - Return(tt.resp, nil).Times(1) - seqData.Mocks.SeqDB = seqDbMock - - profilesRepoMock := mock_repo.NewMockUserProfiles(ctrl) - profilesRepoMock.EXPECT().GetOrCreate(gomock.Any(), tt.mockArgs.profilesReq). - Return(tt.mockArgs.profilesResp, tt.mockArgs.profilesErr).Times(1) - seqData.Mocks.ProfilesRepo = profilesRepoMock + svcMock.EXPECT(). + StartAsyncSearch(gomock.Any(), tt.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) - asyncSearchesRepoMock := mock_repo.NewMockAsyncSearches(ctrl) - asyncSearchesRepoMock.EXPECT().SaveAsyncSearch(gomock.Any(), gomock.Any()). - Return(tt.mockArgs.repoErr).Times(1) - seqData.Mocks.AsyncSearchesRepo = asyncSearchesRepoMock + seqData.Mocks.AsyncSearchesSvc = svcMock } - api := initTestAPIWithAsyncSearches(seqData) + api := setupTestAPI(seqData) + got, err := api.StartAsyncSearch(context.Background(), tt.req) - ctx := context.Background() - ctx = context.WithValue(ctx, types.UserKey{}, mockUserName) - - resp, err := api.StartAsyncSearch(ctx, tt.req) - require.NoError(t, err) - - require.True(t, proto.Equal(tt.resp, resp)) + require.Equal(t, tt.wantCode, status.Code(err)) + if tt.wantCode != codes.OK { + return + } + require.Equal(t, tt.want, got) }) } } func TestServeStartAsyncSearch_Disabled(t *testing.T) { seqData := test.APITestData{} - api := initTestAPI(seqData) + api := setupTestAPI(seqData) _, err := api.StartAsyncSearch(context.Background(), &seqapi.StartAsyncSearchRequest{}) require.Error(t, err) diff --git a/internal/api/seqapi/v1/grpc/test_data.go b/internal/api/seqapi/v1/grpc/test_data.go index 4b605ea..a088897 100644 --- a/internal/api/seqapi/v1/grpc/test_data.go +++ b/internal/api/seqapi/v1/grpc/test_data.go @@ -1,18 +1,23 @@ package grpc import ( - "context" + "errors" + "time" - "github.com/ozontech/seq-ui/internal/api/profiles" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" "github.com/ozontech/seq-ui/internal/app/config" "github.com/ozontech/seq-ui/internal/pkg/client/seqdb" - "github.com/ozontech/seq-ui/internal/pkg/repository" - "github.com/ozontech/seq-ui/internal/pkg/service" asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches" ) -func initTestAPI(data test.APITestData) *API { +// Shared test data. +var ( + errSomethingWrong = errors.New("something happened wrong") + testSearchID = "69e4a4a6-0922-43bd-952d-060a86c2b622" + testTimestamp = time.Date(2023, time.September, 25, 10, 20, 30, 0, time.UTC) +) + +func setupTestAPI(data test.APITestData) *API { // when test cases don't explicitly provide configuration if data.Cfg.SeqAPIOptions == nil { data.Cfg.SeqAPIOptions = &config.SeqAPIOptions{} @@ -24,20 +29,10 @@ func initTestAPI(data test.APITestData) *API { seqDBClients[envConfig.SeqDB] = data.Mocks.SeqDB } - return New(data.Cfg, seqDBClients, data.Mocks.Cache, data.Mocks.Cache, nil, nil) -} - -func initTestAPIWithAsyncSearches(data test.APITestData) *API { - if data.Cfg.SeqAPIOptions == nil { - data.Cfg.SeqAPIOptions = &config.SeqAPIOptions{} + var asyncSvc asyncsearches.Service + if data.Mocks.AsyncSearchesSvc != nil { + asyncSvc = data.Mocks.AsyncSearchesSvc } - seqDBClients := map[string]seqdb.Client{ - config.DefaultSeqDBClientID: data.Mocks.SeqDB, - } - as := asyncsearches.New(context.Background(), data.Mocks.AsyncSearchesRepo, data.Mocks.SeqDB, data.AsyncCfg) - s := service.New(&repository.Repository{ - UserProfiles: data.Mocks.ProfilesRepo, - }) - p := profiles.New(s) - return New(data.Cfg, seqDBClients, data.Mocks.Cache, data.Mocks.Cache, as, p) + + return New(data.Cfg, seqDBClients, data.Mocks.Cache, data.Mocks.Cache, asyncSvc) } diff --git a/internal/api/seqapi/v1/http/aggregation_test.go b/internal/api/seqapi/v1/http/aggregation_test.go index d567c8a..6a675f4 100644 --- a/internal/api/seqapi/v1/http/aggregation_test.go +++ b/internal/api/seqapi/v1/http/aggregation_test.go @@ -1,16 +1,11 @@ package http import ( - "encoding/json" "errors" - "fmt" "net/http" - "net/http/httptest" - "strings" "testing" "time" - "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "google.golang.org/protobuf/types/known/timestamppb" @@ -22,21 +17,6 @@ import ( ) func TestServeGetAggregation(t *testing.T) { - query := "message:error" - from := time.Date(2023, time.September, 25, 10, 20, 30, 0, time.UTC) - to := from.Add(time.Second) - - formatReqBody := func(aggField string, aggQueries aggregationQueries) string { - if len(aggQueries) > 0 { - aggQueriesRaw, err := json.Marshal(aggQueries) - assert.NoError(t, err) - return fmt.Sprintf(`{"query":%q,"from":%q,"to":%q,"aggField":%q,"aggregations":%s}`, - query, from.Format(time.RFC3339), to.Format(time.RFC3339), aggField, aggQueriesRaw) - } - return fmt.Sprintf(`{"query":%q,"from":%q,"to":%q,"aggField":%q}`, - query, from.Format(time.RFC3339), to.Format(time.RFC3339), aggField) - } - type mockArgs struct { req *seqapi.GetAggregationRequest resp *seqapi.GetAggregationResponse @@ -46,21 +26,26 @@ func TestServeGetAggregation(t *testing.T) { tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req getAggregationRequest + want getAggregationResponse + wantErr bool mockArgs *mockArgs cfg config.SeqAPI }{ { - name: "ok_single_agg", - reqBody: formatReqBody("test_single", nil), + name: "ok_single_agg", + req: getAggregationRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + AggField: "test_single", + }, mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), AggField: "test_single", }, resp: &seqapi.GetAggregationResponse{ @@ -71,8 +56,12 @@ func TestServeGetAggregation(t *testing.T) { }, }, }, - wantRespBody: `{"aggregation":{"buckets":[{"key":"test1","value":1},{"key":"test2","value":2}]},"aggregations":[{"buckets":[{"key":"test1","value":1},{"key":"test2","value":2}]}],"error":{"code":"ERROR_CODE_NO"},"partialResponse":false}`, - wantStatus: http.StatusOK, + want: getAggregationResponse{ + Aggregation: aggregationFromProto(test.MakeAggregation(2, nil)), + Aggregations: aggregationsFromProto(test.MakeAggregations(1, 2, nil), true), + Error: apiError{Code: aecNo}, + PartialResponse: false, + }, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxAggregationsPerRequest: 3, @@ -81,16 +70,21 @@ func TestServeGetAggregation(t *testing.T) { }, { name: "ok_multi_agg", - reqBody: formatReqBody("", aggregationQueries{ - {Field: "test_multi1"}, - {Field: "test_multi2"}, - {Field: "test_multi3"}, - }), + req: getAggregationRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Aggregations: aggregationQueries{ + {Field: "test_multi1"}, + {Field: "test_multi2"}, + {Field: "test_multi3"}, + }, + }, mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Aggregations: []*seqapi.AggregationQuery{ {Field: "test_multi1"}, {Field: "test_multi2"}, @@ -105,8 +99,12 @@ func TestServeGetAggregation(t *testing.T) { }, }, }, - wantRespBody: `{"aggregation":{"buckets":[{"key":"test1","value":1},{"key":"test2","value":2},{"key":"test3","value":3}]},"aggregations":[{"buckets":[{"key":"test1","value":1},{"key":"test2","value":2},{"key":"test3","value":3}]},{"buckets":[{"key":"test1","value":1},{"key":"test2","value":2},{"key":"test3","value":3}]}],"error":{"code":"ERROR_CODE_NO"},"partialResponse":false}`, - wantStatus: http.StatusOK, + want: getAggregationResponse{ + Aggregation: aggregationFromProto(test.MakeAggregation(3, nil)), + Aggregations: aggregationsFromProto(test.MakeAggregations(2, 3, nil), true), + Error: apiError{Code: aecNo}, + PartialResponse: false, + }, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxAggregationsPerRequest: 3, @@ -116,19 +114,24 @@ func TestServeGetAggregation(t *testing.T) { { name: "ok_agg_quantile", - reqBody: formatReqBody("", aggregationQueries{ - { - Field: "test_multi1", - GroupBy: "service", - Func: afQuantile, - Quantiles: []float64{0.95, 0.99}, - }, - }), + req: getAggregationRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Aggregations: aggregationQueries{ + { + Field: "test_multi1", + GroupBy: "service", + Func: afQuantile, + Quantiles: []float64{0.95, 0.99}, + }, + }, + }, mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Aggregations: []*seqapi.AggregationQuery{ { Field: "test_multi1", @@ -152,8 +155,18 @@ func TestServeGetAggregation(t *testing.T) { }, }, }, - wantRespBody: `{"aggregation":{"buckets":[{"key":"test1","value":1,"not_exists":10,"quantiles":[100,150]},{"key":"test2","value":2,"not_exists":10,"quantiles":[100,150]},{"key":"test3","value":3,"not_exists":10,"quantiles":[100,150]}]},"aggregations":[{"buckets":[{"key":"test1","value":1,"not_exists":10,"quantiles":[100,150]},{"key":"test2","value":2,"not_exists":10,"quantiles":[100,150]},{"key":"test3","value":3,"not_exists":10,"quantiles":[100,150]}]},{"buckets":[{"key":"test1","value":1,"not_exists":10,"quantiles":[100,150]},{"key":"test2","value":2,"not_exists":10,"quantiles":[100,150]},{"key":"test3","value":3,"not_exists":10,"quantiles":[100,150]}]}],"error":{"code":"ERROR_CODE_NO"},"partialResponse":false}`, - wantStatus: http.StatusOK, + want: getAggregationResponse{ + Aggregation: aggregationFromProto(test.MakeAggregation(3, &test.MakeAggOpts{ + NotExists: 10, + Quantiles: []float64{100, 150}, + })), + Aggregations: aggregationsFromProto(test.MakeAggregations(2, 3, &test.MakeAggOpts{ + NotExists: 10, + Quantiles: []float64{100, 150}, + }), true), + Error: apiError{Code: aecNo}, + PartialResponse: false, + }, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxAggregationsPerRequest: 3, @@ -161,13 +174,18 @@ func TestServeGetAggregation(t *testing.T) { }, }, { - name: "err_partial_response", - reqBody: formatReqBody("test_err_partial", nil), + name: "err_partial_response", + req: getAggregationRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + AggField: "test_err_partial", + }, mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), AggField: "test_err_partial", }, resp: &seqapi.GetAggregationResponse{ @@ -178,8 +196,12 @@ func TestServeGetAggregation(t *testing.T) { PartialResponse: true, }, }, - wantRespBody: `{"aggregation":{"buckets":[]},"aggregations":[],"error":{"code":"ERROR_CODE_PARTIAL_RESPONSE","message":"partial response"},"partialResponse":true}`, - wantStatus: http.StatusOK, + want: getAggregationResponse{ + Aggregation: aggregationFromProto(nil), + Aggregations: aggregationsFromProto(nil, true), + Error: apiError{Code: aecPartialResponse, Message: "partial response"}, + PartialResponse: true, + }, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxAggregationsPerRequest: 3, @@ -187,14 +209,14 @@ func TestServeGetAggregation(t *testing.T) { }, }, { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, - }, - { - name: "err_aggs_limit_max", - reqBody: formatReqBody("", aggregationQueries{{}, {}, {}}), - wantStatus: http.StatusBadRequest, + name: "err_aggs_limit_max", + req: getAggregationRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Aggregations: aggregationQueries{{}, {}, {}}, + }, + wantErr: true, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxAggregationsPerRequest: 2, @@ -202,18 +224,23 @@ func TestServeGetAggregation(t *testing.T) { }, }, { - name: "err_client", - reqBody: formatReqBody("test_err_client", nil), + name: "err_client", + req: getAggregationRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + AggField: "test_err_client", + }, mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), AggField: "test_err_client", }, err: errors.New("client error"), }, - wantStatus: http.StatusInternalServerError, + wantErr: true, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxAggregationsPerRequest: 3, @@ -221,8 +248,8 @@ func TestServeGetAggregation(t *testing.T) { }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -232,21 +259,25 @@ func TestServeGetAggregation(t *testing.T) { if tt.mockArgs != nil { ctrl := gomock.NewController(t) - seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().GetAggregation(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) + + seqDbMock.EXPECT(). + GetAggregation(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) seqData.Mocks.SeqDB = seqDbMock } - api := initTestAPI(seqData) - req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/aggregation", strings.NewReader(tt.reqBody)) - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetAggregation, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + api := setupTestAPI(seqData) + + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[getAggregationRequest, getAggregationResponse]{ + Method: http.MethodPost, + Target: "/seqapi/v1/aggregation", + Req: tt.req, + Handler: api.serveGetAggregation, + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/seqapi/v1/http/aggregation_ts_test.go b/internal/api/seqapi/v1/http/aggregation_ts_test.go index a7b20fa..9c4f86c 100644 --- a/internal/api/seqapi/v1/http/aggregation_ts_test.go +++ b/internal/api/seqapi/v1/http/aggregation_ts_test.go @@ -23,9 +23,6 @@ import ( ) func TestServeGetAggregationTs(t *testing.T) { - query := "message:error" - from := time.Date(2023, time.September, 25, 10, 20, 30, 0, time.UTC) - to := from.Add(5 * time.Second) interval := "1s" interval2 := "3000ms" targetBucketRate := "2s" @@ -34,7 +31,7 @@ func TestServeGetAggregationTs(t *testing.T) { aggQueriesRaw, err := json.Marshal(aggQueries) assert.NoError(t, err) return fmt.Sprintf(`{"query":%q,"from":%q,"to":%q,"aggregations":%s}`, - query, from.Format(time.RFC3339), to.Format(time.RFC3339), aggQueriesRaw) + testQuery, testTimestamp.Format(time.RFC3339), testTimestamp.Add(time.Second).Format(time.RFC3339), aggQueriesRaw) } type mockArgs struct { @@ -73,9 +70,9 @@ func TestServeGetAggregationTs(t *testing.T) { }), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Aggregations: []*seqapi.AggregationQuery{ {Field: "test_count1", Func: seqapi.AggFunc_AGG_FUNC_COUNT, Interval: &interval}, {Field: "test_count2", Func: seqapi.AggFunc_AGG_FUNC_COUNT, Interval: &interval}, @@ -84,9 +81,9 @@ func TestServeGetAggregationTs(t *testing.T) { resp: &seqapi.GetAggregationResponse{ Aggregation: test.MakeAggregation(3, &test.MakeAggOpts{ Ts: []*timestamppb.Timestamp{ - timestamppb.New(from.Add(time.Second)), - timestamppb.New(from.Add(2 * time.Second)), - timestamppb.New(from.Add(3 * time.Second)), + timestamppb.New(testTimestamp.Add(time.Second)), + timestamppb.New(testTimestamp.Add(2 * time.Second)), + timestamppb.New(testTimestamp.Add(3 * time.Second)), }, Values: []float64{ 1, @@ -96,9 +93,9 @@ func TestServeGetAggregationTs(t *testing.T) { }), Aggregations: test.MakeAggregations(2, 3, &test.MakeAggOpts{ Ts: []*timestamppb.Timestamp{ - timestamppb.New(from.Add(time.Second)), - timestamppb.New(from.Add(2 * time.Second)), - timestamppb.New(from.Add(3 * time.Second)), + timestamppb.New(testTimestamp.Add(time.Second)), + timestamppb.New(testTimestamp.Add(2 * time.Second)), + timestamppb.New(testTimestamp.Add(3 * time.Second)), }, }), Error: &seqapi.Error{ @@ -136,9 +133,9 @@ func TestServeGetAggregationTs(t *testing.T) { }), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Aggregations: []*seqapi.AggregationQuery{ {Field: "test_count1", Func: seqapi.AggFunc_AGG_FUNC_COUNT, Interval: &interval2}, {Field: "test_count2", Func: seqapi.AggFunc_AGG_FUNC_COUNT, Interval: &interval2, TargetBucketRate: &targetBucketRate}, @@ -147,9 +144,9 @@ func TestServeGetAggregationTs(t *testing.T) { resp: &seqapi.GetAggregationResponse{ Aggregations: test.MakeAggregations(2, 3, &test.MakeAggOpts{ Ts: []*timestamppb.Timestamp{ - timestamppb.New(from.Add(time.Second)), - timestamppb.New(from.Add(2 * time.Second)), - timestamppb.New(from.Add(3 * time.Second)), + timestamppb.New(testTimestamp.Add(time.Second)), + timestamppb.New(testTimestamp.Add(2 * time.Second)), + timestamppb.New(testTimestamp.Add(3 * time.Second)), }, Values: []float64{ 3, @@ -186,9 +183,9 @@ func TestServeGetAggregationTs(t *testing.T) { }), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Aggregations: []*seqapi.AggregationQuery{ { Field: "test_quantile1", @@ -203,17 +200,17 @@ func TestServeGetAggregationTs(t *testing.T) { Aggregation: test.MakeAggregation(3, &test.MakeAggOpts{ Quantiles: []float64{100, 150}, Ts: []*timestamppb.Timestamp{ - timestamppb.New(from.Add(time.Second)), - timestamppb.New(from.Add(2 * time.Second)), - timestamppb.New(from.Add(3 * time.Second)), + timestamppb.New(testTimestamp.Add(time.Second)), + timestamppb.New(testTimestamp.Add(2 * time.Second)), + timestamppb.New(testTimestamp.Add(3 * time.Second)), }, }), Aggregations: test.MakeAggregations(1, 3, &test.MakeAggOpts{ Quantiles: []float64{100, 150}, Ts: []*timestamppb.Timestamp{ - timestamppb.New(from.Add(time.Second)), - timestamppb.New(from.Add(2 * time.Second)), - timestamppb.New(from.Add(3 * time.Second)), + timestamppb.New(testTimestamp.Add(time.Second)), + timestamppb.New(testTimestamp.Add(2 * time.Second)), + timestamppb.New(testTimestamp.Add(3 * time.Second)), }, }), Error: &seqapi.Error{ @@ -235,9 +232,9 @@ func TestServeGetAggregationTs(t *testing.T) { reqBody: formatReqBody(nil), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), }, resp: &seqapi.GetAggregationResponse{ Error: &seqapi.Error{ @@ -283,7 +280,7 @@ func TestServeGetAggregationTs(t *testing.T) { cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxAggregationsPerRequest: 3, - MaxBucketsPerAggregationTs: 8, + MaxBucketsPerAggregationTs: 1, }, }, }, @@ -292,9 +289,9 @@ func TestServeGetAggregationTs(t *testing.T) { reqBody: formatReqBody(nil), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), }, err: errors.New("client error"), }, @@ -324,7 +321,7 @@ func TestServeGetAggregationTs(t *testing.T) { seqData.Mocks.SeqDB = seqDbMock } - api := initTestAPI(seqData) + api := setupTestAPI(seqData) req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/aggregation_ts", strings.NewReader(tt.reqBody)) httputil.DoTestHTTP(t, httputil.TestDataHTTP{ diff --git a/internal/api/seqapi/v1/http/api.go b/internal/api/seqapi/v1/http/api.go index 1dda605..c762883 100644 --- a/internal/api/seqapi/v1/http/api.go +++ b/internal/api/seqapi/v1/http/api.go @@ -10,7 +10,6 @@ import ( "github.com/gofrs/uuid" "go.uber.org/zap" - "github.com/ozontech/seq-ui/internal/api/profiles" "github.com/ozontech/seq-ui/internal/app/config" "github.com/ozontech/seq-ui/internal/app/tokenlimiter" "github.com/ozontech/seq-ui/internal/app/types" @@ -39,8 +38,7 @@ type API struct { inmemWithRedisCache cache.Cache redisCache cache.Cache nowFn func() time.Time - asyncSearches *asyncsearches.Service - profiles *profiles.Profiles + asyncSearches asyncsearches.Service envsResponse getEnvsResponse } @@ -49,8 +47,7 @@ func New( seqDBСlients map[string]seqdb.Client, inmemWithRedisCache cache.Cache, redisCache cache.Cache, - asyncSearches *asyncsearches.Service, - p *profiles.Profiles, + asyncSearches asyncsearches.Service, ) *API { var globalfCache *fieldsCache if cfg.FieldsCacheTTL > 0 { @@ -135,7 +132,6 @@ func New( redisCache: redisCache, nowFn: time.Now, asyncSearches: asyncSearches, - profiles: p, envsResponse: parseEnvs(cfg), } } diff --git a/internal/api/seqapi/v1/http/cancel_async_search.go b/internal/api/seqapi/v1/http/cancel_async_search.go index 2c8c4f8..b07776d 100644 --- a/internal/api/seqapi/v1/http/cancel_async_search.go +++ b/internal/api/seqapi/v1/http/cancel_async_search.go @@ -47,15 +47,7 @@ func (a *API) serveCancelAsyncSearch(w http.ResponseWriter, r *http.Request) { }, ) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - httputil.ProcessError(wr, err) - return - } - - _, err = a.asyncSearches.CancelAsyncSearch(ctx, profileID, &seqapi.CancelAsyncSearchRequest{ - SearchId: searchID, - }) + _, err := a.asyncSearches.CancelAsyncSearch(ctx, &seqapi.CancelAsyncSearchRequest{SearchId: searchID}) if err != nil { status := http.StatusInternalServerError if errors.Is(err, types.ErrPermissionDenied) { diff --git a/internal/api/seqapi/v1/http/cancel_async_search_test.go b/internal/api/seqapi/v1/http/cancel_async_search_test.go index 053df8b..e904a2b 100644 --- a/internal/api/seqapi/v1/http/cancel_async_search_test.go +++ b/internal/api/seqapi/v1/http/cancel_async_search_test.go @@ -1,115 +1,63 @@ package http import ( - "context" "fmt" "net/http" - "net/http/httptest" "testing" - "github.com/go-chi/chi/v5" "go.uber.org/mock/gomock" "github.com/ozontech/seq-ui/internal/api/httputil" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" - "github.com/ozontech/seq-ui/internal/app/types" - mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" - mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) func TestServeCancelAsyncSearch(t *testing.T) { - const ( - mockSearchID1 = "69e4a4a6-0922-43bd-952d-060a86c2b622" - mockUserName1 = "some_user_1" - mockUserName2 = "some_user_2" - mockProfileID1 = 1 - mockProfileID2 = 2 - ) - type mockArgs struct { - userName string - - proxyReq *seqapi.CancelAsyncSearchRequest - proxyResp *seqapi.CancelAsyncSearchResponse - proxyErr error - - profilesReq *types.GetOrCreateUserProfileRequest - profilesResp *types.UserProfile - profilesErr error - - repoResp *types.AsyncSearchInfo - repoErr error + req *seqapi.CancelAsyncSearchRequest + resp *seqapi.CancelAsyncSearchResponse + err error } tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + searchID string + wantErr bool + noResp bool mockArgs *mockArgs }{ { - name: "ok", + name: "ok", + searchID: testSearchID, + noResp: true, mockArgs: &mockArgs{ - userName: mockUserName1, - proxyReq: &seqapi.CancelAsyncSearchRequest{ - SearchId: mockSearchID1, - }, - proxyResp: &seqapi.CancelAsyncSearchResponse{}, - profilesReq: &types.GetOrCreateUserProfileRequest{ - UserName: mockUserName1, - }, - profilesResp: &types.UserProfile{ - ID: mockProfileID1, - UserName: mockUserName1, - }, - repoResp: &types.AsyncSearchInfo{ - SearchID: mockSearchID1, - OwnerID: mockProfileID1, - OwnerName: mockUserName1, + req: &seqapi.CancelAsyncSearchRequest{ + SearchId: testSearchID, }, + resp: &seqapi.CancelAsyncSearchResponse{}, }, - wantRespBody: ``, - wantStatus: http.StatusOK, }, { - name: "err_permission_denied", - mockArgs: &mockArgs{ - userName: mockUserName1, - proxyReq: &seqapi.CancelAsyncSearchRequest{ - SearchId: mockSearchID1, - }, - profilesReq: &types.GetOrCreateUserProfileRequest{ - UserName: mockUserName1, - }, - profilesResp: &types.UserProfile{ - ID: mockProfileID1, - UserName: mockUserName1, - }, - repoResp: &types.AsyncSearchInfo{ - SearchID: mockSearchID1, - OwnerID: mockProfileID2, - OwnerName: mockUserName2, - }, - }, - wantRespBody: `{"message":"permission denied: cancel async search"}`, - wantStatus: http.StatusUnauthorized, + name: "invalid_id", + searchID: "some invalid id", + wantErr: true, }, { - name: "invalid id", + name: "err_svc", + searchID: testSearchID, + wantErr: true, mockArgs: &mockArgs{ - userName: mockUserName1, - proxyReq: &seqapi.CancelAsyncSearchRequest{ - SearchId: "some_invalid_id", + req: &seqapi.CancelAsyncSearchRequest{ + SearchId: testSearchID, }, + err: errSomethingWrong, }, - wantRespBody: `{"message":"invalid request field: invalid uuid"}`, - wantStatus: http.StatusBadRequest, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -118,45 +66,26 @@ func TestServeCancelAsyncSearch(t *testing.T) { if tt.mockArgs != nil { ctrl := gomock.NewController(t) + svcMock := mock_asyncsearches.NewMockService(ctrl) - if tt.mockArgs.proxyResp != nil { - seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().CancelAsyncSearch(gomock.Any(), tt.mockArgs.proxyReq). - Return(tt.mockArgs.proxyResp, tt.mockArgs.proxyErr).Times(1) - seqData.Mocks.SeqDB = seqDbMock + if tt.mockArgs.req != nil { + svcMock.EXPECT(). + CancelAsyncSearch(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - if tt.mockArgs.profilesResp != nil { - profilesRepoMock := mock_repo.NewMockUserProfiles(ctrl) - profilesRepoMock.EXPECT().GetOrCreate(gomock.Any(), *tt.mockArgs.profilesReq). - Return(*tt.mockArgs.profilesResp, tt.mockArgs.profilesErr).Times(1) - seqData.Mocks.ProfilesRepo = profilesRepoMock - } - - if tt.mockArgs.repoResp != nil { - asyncSearchesRepoMock := mock_repo.NewMockAsyncSearches(ctrl) - asyncSearchesRepoMock.EXPECT().GetAsyncSearchById(gomock.Any(), tt.mockArgs.proxyReq.SearchId). - Return(*tt.mockArgs.repoResp, tt.mockArgs.repoErr).Times(1) - seqData.Mocks.AsyncSearchesRepo = asyncSearchesRepoMock - } + seqData.Mocks.AsyncSearchesSvc = svcMock } - api := initTestAPIWithAsyncSearches(seqData) - req := httptest.NewRequest( - http.MethodPost, - fmt.Sprintf("/seqapi/v1/async_search/%s/cancel", tt.mockArgs.proxyReq.SearchId), - http.NoBody, - ) - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, tt.mockArgs.userName)) - rCtx := chi.NewRouteContext() - rCtx.URLParams.Add("id", tt.mockArgs.proxyReq.SearchId) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rCtx)) - - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveCancelAsyncSearch, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + api := setupTestAPI(seqData) + + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, struct{}]{ + Method: http.MethodPost, + Target: fmt.Sprintf("/seqapi/v1/async_search/%s/cancel", testSearchID), + Handler: withQueryParamID(api.serveCancelAsyncSearch, tt.searchID), + WantErr: tt.wantErr, + NoResp: tt.noResp, }) }) } @@ -164,17 +93,12 @@ func TestServeCancelAsyncSearch(t *testing.T) { func TestServeCancelAsyncSearch_Disabled(t *testing.T) { seqData := test.APITestData{} - api := initTestAPI(seqData) - req := httptest.NewRequest( - http.MethodPost, - "/seqapi/v1/async_search/c9a34cf8-4c66-484e-9cc2-42979d848656/cancel", - http.NoBody, - ) - - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveCancelAsyncSearch, - WantRespBody: `{"message":"async searches disabled"}`, - WantStatus: http.StatusBadRequest, + api := setupTestAPI(seqData) + + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, struct{}]{ + Method: http.MethodPost, + Target: fmt.Sprintf("/seqapi/v1/async_search/%s/cancel", testSearchID), + Handler: api.serveCancelAsyncSearch, + WantErr: true, }) } diff --git a/internal/api/seqapi/v1/http/cluster_status_test.go b/internal/api/seqapi/v1/http/cluster_status_test.go index 448345e..0151beb 100644 --- a/internal/api/seqapi/v1/http/cluster_status_test.go +++ b/internal/api/seqapi/v1/http/cluster_status_test.go @@ -1,11 +1,8 @@ package http import ( - "errors" "net/http" - "net/http/httptest" "testing" - "time" "go.uber.org/mock/gomock" "google.golang.org/protobuf/types/known/timestamppb" @@ -22,64 +19,70 @@ func TestStatus(t *testing.T) { err error } - type testCase struct { + tests := []struct { name string - wantRespBody string - wantStatus int + want statusResponse + wantErr bool - mockArgs mockArgs - } - - someMoment := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) - - tests := []testCase{ + mockArgs *mockArgs + }{ { name: "ok", - mockArgs: mockArgs{ + want: statusResponse{ + OldestStorageTime: &testTimestamp, + NumberOfStores: 1, + Stores: []storeStatus{ + { + Host: "host-0", + Values: &storeStatusValues{OldestTime: &testTimestamp}, + }, + }, + }, + mockArgs: &mockArgs{ resp: &seqapi.StatusResponse{ NumberOfStores: 1, - OldestStorageTime: timestamppb.New(someMoment), + OldestStorageTime: timestamppb.New(testTimestamp), Stores: []*seqapi.StoreStatus{ { Host: "host-0", - Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(someMoment)}, + Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(testTimestamp)}, }, }, }, }, - wantRespBody: `{"oldest_storage_time":"2020-01-01T00:00:00Z","number_of_stores":1,"stores":[{"host":"host-0","values":{"oldest_time":"2020-01-01T00:00:00Z"}}]}`, - wantStatus: http.StatusOK, }, { - name: "err_client", - mockArgs: mockArgs{ - err: errors.New("client error"), + name: "err_client", + wantErr: true, + mockArgs: &mockArgs{ + err: errSomethingWrong, }, - wantStatus: http.StatusInternalServerError, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) seqData := test.APITestData{} - + ctrl := gomock.NewController(t) seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().Status(gomock.Any(), gomock.Any()). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - seqData.Mocks.SeqDB = seqDbMock - api := initTestAPI(seqData) - req := httptest.NewRequest(http.MethodGet, "/seqapi/v1/status", http.NoBody) + seqDbMock.EXPECT(). + Status(gomock.Any(), gomock.Any()). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) + + seqData.Mocks.SeqDB = seqDbMock + api := setupTestAPI(seqData) - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveStatus, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, statusResponse]{ + Method: http.MethodGet, + Target: "/seqapi/v1/status", + Handler: api.serveStatus, + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/seqapi/v1/http/delete_async_search.go b/internal/api/seqapi/v1/http/delete_async_search.go index addf2b9..28e64a7 100644 --- a/internal/api/seqapi/v1/http/delete_async_search.go +++ b/internal/api/seqapi/v1/http/delete_async_search.go @@ -47,15 +47,7 @@ func (a *API) serveDeleteAsyncSearch(w http.ResponseWriter, r *http.Request) { }, ) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - httputil.ProcessError(wr, err) - return - } - - _, err = a.asyncSearches.DeleteAsyncSearch(ctx, profileID, &seqapi.DeleteAsyncSearchRequest{ - SearchId: searchID, - }) + _, err := a.asyncSearches.DeleteAsyncSearch(ctx, &seqapi.DeleteAsyncSearchRequest{SearchId: searchID}) if err != nil { status := http.StatusInternalServerError if errors.Is(err, types.ErrPermissionDenied) { diff --git a/internal/api/seqapi/v1/http/delete_async_search_test.go b/internal/api/seqapi/v1/http/delete_async_search_test.go index 878adf6..a3802db 100644 --- a/internal/api/seqapi/v1/http/delete_async_search_test.go +++ b/internal/api/seqapi/v1/http/delete_async_search_test.go @@ -1,119 +1,63 @@ package http import ( - "context" "fmt" "net/http" - "net/http/httptest" "testing" - "github.com/go-chi/chi/v5" "go.uber.org/mock/gomock" "github.com/ozontech/seq-ui/internal/api/httputil" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" - "github.com/ozontech/seq-ui/internal/app/types" - mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" - mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) func TestServeDeleteAsyncSearch(t *testing.T) { - const ( - mockSearchID1 = "69e4a4a6-0922-43bd-952d-060a86c2b622" - mockUserName1 = "some_user_1" - mockUserName2 = "some_user_2" - mockProfileID1 = 1 - mockProfileID2 = 2 - ) - type mockArgs struct { - userName string - - proxyReq *seqapi.DeleteAsyncSearchRequest - proxyResp *seqapi.DeleteAsyncSearchResponse - proxyErr error - - profilesReq *types.GetOrCreateUserProfileRequest - profilesResp *types.UserProfile - profilesErr error - - repoGetAsyncSearchResp *types.AsyncSearchInfo - repoGetAsyncSearchErr error - - repoDeleteAsyncSearchErr error + req *seqapi.DeleteAsyncSearchRequest + resp *seqapi.DeleteAsyncSearchResponse + err error } tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int - shouldDelete bool + searchID string + wantErr bool + noResp bool mockArgs *mockArgs }{ { - name: "ok", + name: "ok", + searchID: testSearchID, + noResp: true, mockArgs: &mockArgs{ - userName: mockUserName1, - proxyReq: &seqapi.DeleteAsyncSearchRequest{ - SearchId: mockSearchID1, - }, - proxyResp: &seqapi.DeleteAsyncSearchResponse{}, - profilesReq: &types.GetOrCreateUserProfileRequest{ - UserName: mockUserName1, - }, - profilesResp: &types.UserProfile{ - ID: mockProfileID1, - UserName: mockUserName1, - }, - repoGetAsyncSearchResp: &types.AsyncSearchInfo{ - SearchID: mockSearchID1, - OwnerID: mockProfileID1, - OwnerName: mockUserName1, + req: &seqapi.DeleteAsyncSearchRequest{ + SearchId: testSearchID, }, + resp: &seqapi.DeleteAsyncSearchResponse{}, }, - shouldDelete: true, - wantRespBody: ``, - wantStatus: http.StatusOK, }, { - name: "err_permission_denied", - mockArgs: &mockArgs{ - userName: mockUserName1, - proxyReq: &seqapi.DeleteAsyncSearchRequest{ - SearchId: mockSearchID1, - }, - profilesReq: &types.GetOrCreateUserProfileRequest{ - UserName: mockUserName1, - }, - profilesResp: &types.UserProfile{ - ID: mockProfileID1, - UserName: mockUserName1, - }, - repoGetAsyncSearchResp: &types.AsyncSearchInfo{ - SearchID: mockSearchID1, - OwnerID: mockProfileID2, - OwnerName: mockUserName2, - }, - }, - wantRespBody: `{"message":"permission denied: delete async search"}`, - wantStatus: http.StatusUnauthorized, + name: "invalid_id", + searchID: "some invalid id", + wantErr: true, }, { - name: "invalid id", + name: "err_svc", + searchID: testSearchID, + wantErr: true, mockArgs: &mockArgs{ - userName: mockUserName1, - proxyReq: &seqapi.DeleteAsyncSearchRequest{ - SearchId: "some_invalid_id", + req: &seqapi.DeleteAsyncSearchRequest{ + SearchId: testSearchID, }, + err: errSomethingWrong, }, - wantRespBody: `{"message":"invalid request field: invalid uuid"}`, - wantStatus: http.StatusBadRequest, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -123,50 +67,23 @@ func TestServeDeleteAsyncSearch(t *testing.T) { if tt.mockArgs != nil { ctrl := gomock.NewController(t) - if tt.mockArgs.proxyResp != nil { - seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().DeleteAsyncSearch(gomock.Any(), tt.mockArgs.proxyReq). - Return(tt.mockArgs.proxyResp, tt.mockArgs.proxyErr).Times(1) - seqData.Mocks.SeqDB = seqDbMock - } - - if tt.mockArgs.profilesResp != nil { - profilesRepoMock := mock_repo.NewMockUserProfiles(ctrl) - profilesRepoMock.EXPECT().GetOrCreate(gomock.Any(), *tt.mockArgs.profilesReq). - Return(*tt.mockArgs.profilesResp, tt.mockArgs.profilesErr).Times(1) - seqData.Mocks.ProfilesRepo = profilesRepoMock - } - - if tt.mockArgs.repoGetAsyncSearchResp != nil { - asyncSearchesRepoMock := mock_repo.NewMockAsyncSearches(ctrl) - asyncSearchesRepoMock.EXPECT().GetAsyncSearchById(gomock.Any(), tt.mockArgs.proxyReq.SearchId). - Return(*tt.mockArgs.repoGetAsyncSearchResp, tt.mockArgs.repoGetAsyncSearchErr).Times(1) - - if tt.shouldDelete { - asyncSearchesRepoMock.EXPECT().DeleteAsyncSearch(gomock.Any(), tt.mockArgs.proxyReq.SearchId). - Return(tt.mockArgs.repoDeleteAsyncSearchErr).Times(1) - } - - seqData.Mocks.AsyncSearchesRepo = asyncSearchesRepoMock - } + svcMock := mock_asyncsearches.NewMockService(ctrl) + svcMock.EXPECT(). + DeleteAsyncSearch(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) + + seqData.Mocks.AsyncSearchesSvc = svcMock } - api := initTestAPIWithAsyncSearches(seqData) - req := httptest.NewRequest( - http.MethodDelete, - fmt.Sprintf("/seqapi/v1/async_search/%s", tt.mockArgs.proxyReq.SearchId), - http.NoBody, - ) - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, tt.mockArgs.userName)) - rCtx := chi.NewRouteContext() - rCtx.URLParams.Add("id", tt.mockArgs.proxyReq.SearchId) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rCtx)) - - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveDeleteAsyncSearch, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + api := setupTestAPI(seqData) + + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, struct{}]{ + Method: http.MethodDelete, + Target: fmt.Sprintf("/seqapi/v1/async_search/%s", testSearchID), + Handler: withQueryParamID(api.serveDeleteAsyncSearch, tt.searchID), + WantErr: tt.wantErr, + NoResp: tt.noResp, }) }) } @@ -174,17 +91,12 @@ func TestServeDeleteAsyncSearch(t *testing.T) { func TestServeDeleteAsyncSearch_Disabled(t *testing.T) { seqData := test.APITestData{} - api := initTestAPI(seqData) - req := httptest.NewRequest( - http.MethodDelete, - "/seqapi/v1/async_search/c9a34cf8-4c66-484e-9cc2-42979d848656", - http.NoBody, - ) - - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveDeleteAsyncSearch, - WantRespBody: `{"message":"async searches disabled"}`, - WantStatus: http.StatusBadRequest, + api := setupTestAPI(seqData) + + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, struct{}]{ + Method: http.MethodDelete, + Target: fmt.Sprintf("/seqapi/v1/async_search/%s", testSearchID), + Handler: api.serveDeleteAsyncSearch, + WantErr: true, }) } diff --git a/internal/api/seqapi/v1/http/events_test.go b/internal/api/seqapi/v1/http/events_test.go index 5646dbf..7b33966 100644 --- a/internal/api/seqapi/v1/http/events_test.go +++ b/internal/api/seqapi/v1/http/events_test.go @@ -1,16 +1,12 @@ package http import ( - "context" - "encoding/json" "errors" "fmt" "net/http" - "net/http/httptest" "testing" "time" - "github.com/go-chi/chi/v5" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "google.golang.org/protobuf/proto" @@ -25,45 +21,47 @@ import ( ) func TestServeGetEvent(t *testing.T) { - type seqDBArgs struct { - req *seqapi.GetEventRequest - resp *seqapi.GetEventResponse - err error - } + var ( + id1 = "test1" + id2 = "test2" + id3 = "test3" + id4 = "test4" + cacheTTL = time.Minute + ) - eventTime := time.Date(2024, time.December, 31, 10, 20, 30, 400000, time.UTC) // 2024-12-31T10:20:30.0004Z - id1 := "test1" - id2 := "test2" - id3 := "test3" - id4 := "test4" - event1 := test.MakeEvent(id1, 1, eventTime) + event1 := test.MakeEvent(id1, 1, testTimestamp) event1json, _ := proto.Marshal(event1) - event2 := test.MakeEvent(id2, 2, eventTime) + event2 := test.MakeEvent(id2, 2, testTimestamp) event2json, _ := proto.Marshal(event2) - event3 := test.MakeEvent(id3, 0, eventTime) + event3 := test.MakeEvent(id3, 0, testTimestamp) event3json, _ := proto.Marshal(event3) - err := errors.New("test error") - cacheTTL := time.Minute + + type mockArgs struct { + req *seqapi.GetEventRequest + resp *seqapi.GetEventResponse + err error + } tests := []struct { name string - id string - wantRespBody string - wantStatus int + id string + want getEventResponse + wantErr bool cacheArgs test.CacheMockArgs - seqDBArgs *seqDBArgs + mockArgs *mockArgs }{ { name: "ok_no_cached", id: id1, + want: getEventResponse{Event: eventFromProto(event1)}, cacheArgs: test.CacheMockArgs{ Key: id1, Value: string(event1json), - Err: err, + Err: errSomethingWrong, }, - seqDBArgs: &seqDBArgs{ + mockArgs: &mockArgs{ req: &seqapi.GetEventRequest{ Id: id1, }, @@ -71,28 +69,26 @@ func TestServeGetEvent(t *testing.T) { Event: event1, }, }, - wantRespBody: `{"event":{"id":"test1","data":{"field1":"val1"},"time":"2024-12-31T10:20:30.0004Z"}}`, - wantStatus: http.StatusOK, }, { name: "ok_cached", id: id2, + want: getEventResponse{Event: eventFromProto(event2)}, cacheArgs: test.CacheMockArgs{ Key: id2, Value: string(event2json), }, - wantRespBody: `{"event":{"id":"test2","data":{"field1":"val1","field2":"val2"},"time":"2024-12-31T10:20:30.0004Z"}}`, - wantStatus: http.StatusOK, }, { name: "ok_empty", id: id3, + want: getEventResponse{Event: eventFromProto(event3)}, cacheArgs: test.CacheMockArgs{ Key: id3, Value: string(event3json), - Err: err, + Err: errSomethingWrong, }, - seqDBArgs: &seqDBArgs{ + mockArgs: &mockArgs{ req: &seqapi.GetEventRequest{ Id: id3, }, @@ -100,31 +96,29 @@ func TestServeGetEvent(t *testing.T) { Event: event3, }, }, - wantRespBody: `{"event":{"id":"test3","data":{},"time":"2024-12-31T10:20:30.0004Z"}}`, - wantStatus: http.StatusOK, }, { - name: "err_client", - id: id4, + name: "err_client", + id: id4, + wantErr: true, cacheArgs: test.CacheMockArgs{ Key: id4, - Err: err, + Err: errSomethingWrong, }, - seqDBArgs: &seqDBArgs{ + mockArgs: &mockArgs{ req: &seqapi.GetEventRequest{ Id: id4, }, err: errors.New("client error"), }, - wantStatus: http.StatusInternalServerError, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) + ctrl := gomock.NewController(t) seqData := test.APITestData{ Cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ @@ -134,55 +128,57 @@ func TestServeGetEvent(t *testing.T) { } cacheMock := mock_cache.NewMockCache(ctrl) - cacheMock.EXPECT().Get(gomock.Any(), tt.cacheArgs.Key). - Return(tt.cacheArgs.Value, tt.cacheArgs.Err).Times(1) + cacheMock.EXPECT(). + Get(gomock.Any(), tt.cacheArgs.Key). + Return(tt.cacheArgs.Value, tt.cacheArgs.Err). + Times(1) seqData.Mocks.Cache = cacheMock - if tt.seqDBArgs != nil { + if tt.mockArgs != nil { seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().GetEvent(gomock.Any(), tt.seqDBArgs.req). - Return(tt.seqDBArgs.resp, tt.seqDBArgs.err).Times(1) + seqDbMock.EXPECT(). + GetEvent(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) seqData.Mocks.SeqDB = seqDbMock - if tt.seqDBArgs.err == nil { - cacheMock.EXPECT().SetWithTTL(gomock.Any(), tt.cacheArgs.Key, tt.cacheArgs.Value, cacheTTL). - Return(nil).Times(1) + if tt.mockArgs.err == nil { + cacheMock.EXPECT(). + SetWithTTL(gomock.Any(), tt.cacheArgs.Key, tt.cacheArgs.Value, cacheTTL). + Return(nil). + Times(1) } } - api := initTestAPI(seqData) - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/seqapi/v1/events/%s", tt.id), http.NoBody) - rCtx := chi.NewRouteContext() - rCtx.URLParams.Add("id", tt.id) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rCtx)) + api := setupTestAPI(seqData) - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetEvent, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getEventResponse]{ + Method: http.MethodGet, + Target: fmt.Sprintf("/seqapi/v1/events/%s", tt.id), + Handler: withQueryParamID(api.serveGetEvent, tt.id), + Want: tt.want, + WantErr: tt.wantErr, }) }) } } func TestGetEventWithMasking(t *testing.T) { - type seqDBArgs struct { + var ( + errCache = errors.New("test error") + cacheTTL = time.Minute + ) + type mockArgs struct { req *seqapi.GetEventRequest resp *seqapi.GetEventResponse } - eventTime := time.Date(2024, time.December, 31, 10, 20, 30, 400000, time.UTC) // 2024-12-31T10:20:30.0004Z - - cacheErr := errors.New("test error") - cacheTTL := time.Minute - tests := []struct { name string shouldMask bool isCached bool - wantStatus int + wantErr bool maskingCfg *config.Masking }{ @@ -190,7 +186,6 @@ func TestGetEventWithMasking(t *testing.T) { name: "mask_noncached", shouldMask: true, isCached: false, - wantStatus: http.StatusOK, maskingCfg: &config.Masking{ Masks: []config.Mask{ { @@ -216,7 +211,6 @@ func TestGetEventWithMasking(t *testing.T) { name: "mask_from_cache", shouldMask: true, isCached: true, - wantStatus: http.StatusOK, maskingCfg: &config.Masking{ Masks: []config.Mask{ { @@ -242,7 +236,6 @@ func TestGetEventWithMasking(t *testing.T) { name: "do_not_mask_noncached_regex", shouldMask: false, isCached: false, - wantStatus: http.StatusOK, maskingCfg: &config.Masking{ Masks: []config.Mask{ { @@ -268,7 +261,6 @@ func TestGetEventWithMasking(t *testing.T) { name: "do_not_mask_from_cache_regex", shouldMask: false, isCached: true, - wantStatus: http.StatusOK, maskingCfg: &config.Masking{ Masks: []config.Mask{ { @@ -294,7 +286,6 @@ func TestGetEventWithMasking(t *testing.T) { name: "do_not_mask_noncached_field_filter", shouldMask: false, isCached: true, - wantStatus: http.StatusOK, maskingCfg: &config.Masking{ Masks: []config.Mask{ { @@ -320,7 +311,6 @@ func TestGetEventWithMasking(t *testing.T) { name: "do_not_mask_from_cache_field_filter", shouldMask: false, isCached: true, - wantStatus: http.StatusOK, maskingCfg: &config.Masking{ Masks: []config.Mask{ { @@ -348,7 +338,7 @@ func TestGetEventWithMasking(t *testing.T) { id string event *seqapi.Event eventJson []byte - wantResp []byte + want getEventResponse } formEventData := func(i int, shouldMask bool) eventData { @@ -361,20 +351,18 @@ func TestGetEventWithMasking(t *testing.T) { Data: map[string]string{ eventField: eventVal, }, - Time: timestamppb.New(eventTime), + Time: timestamppb.New(testTimestamp), } if shouldMask { event.Data[eventField] = "***" } eventJson, err := proto.Marshal(event) require.NoError(t, err) - wantResp, err := json.Marshal(getEventResponse{Event: eventFromProto(event)}) - require.NoError(t, err) return eventData{ id: id, event: event, eventJson: eventJson, - wantResp: wantResp, + want: getEventResponse{Event: eventFromProto(event)}, } } @@ -384,12 +372,10 @@ func TestGetEventWithMasking(t *testing.T) { } for i, tt := range tests { - i := i - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) + ctrl := gomock.NewController(t) curEData := eventsData[i] curEID := curEData.id @@ -408,40 +394,46 @@ func TestGetEventWithMasking(t *testing.T) { Value: string(curEData.eventJson), } if !tt.isCached { - cacheArgs.Err = cacheErr + cacheArgs.Err = errCache } - cacheMock.EXPECT().Get(gomock.Any(), cacheArgs.Key). - Return(cacheArgs.Value, cacheArgs.Err).Times(1) + cacheMock.EXPECT(). + Get(gomock.Any(), cacheArgs.Key). + Return(cacheArgs.Value, cacheArgs.Err). + Times(1) seqData.Mocks.Cache = cacheMock seqDbMock := mock_seqdb.NewMockClient(ctrl) if !tt.isCached { - seqDBArgs := &seqDBArgs{ + mockArgs := &mockArgs{ req: &seqapi.GetEventRequest{Id: curEData.id}, resp: &seqapi.GetEventResponse{Event: curEData.event}, } - seqDbMock.EXPECT().GetEvent(gomock.Any(), seqDBArgs.req). - Return(seqDBArgs.resp, nil).Times(1) + seqDbMock.EXPECT(). + GetEvent(gomock.Any(), mockArgs.req). + Return(mockArgs.resp, nil). + Times(1) - cacheMock.EXPECT().SetWithTTL(gomock.Any(), cacheArgs.Key, cacheArgs.Value, cacheTTL). - Return(nil).Times(1) + cacheMock.EXPECT(). + SetWithTTL(gomock.Any(), cacheArgs.Key, cacheArgs.Value, cacheTTL). + Return(nil). + Times(1) } if tt.maskingCfg != nil { - seqDbMock.EXPECT().WithMasking(gomock.Any()).Return().Times(1) + seqDbMock.EXPECT(). + WithMasking(gomock.Any()). + Return(). + Times(1) } seqData.Mocks.SeqDB = seqDbMock - api := initTestAPI(seqData) - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/seqapi/v1/events/%s", curEID), http.NoBody) - rCtx := chi.NewRouteContext() - rCtx.URLParams.Add("id", curEID) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rCtx)) + api := setupTestAPI(seqData) - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetEvent, - WantRespBody: string(curEData.wantResp), - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getEventResponse]{ + Method: http.MethodGet, + Target: fmt.Sprintf("/seqapi/v1/events/%s", curEID), + Handler: withQueryParamID(api.serveGetEvent, curEID), + Want: curEData.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/seqapi/v1/http/export_test.go b/internal/api/seqapi/v1/http/export_test.go index 0b339fc..d203aa3 100644 --- a/internal/api/seqapi/v1/http/export_test.go +++ b/internal/api/seqapi/v1/http/export_test.go @@ -1,17 +1,10 @@ package http import ( - "encoding/json" - "errors" - "fmt" "net/http" - "net/http/httptest" - "strings" "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "google.golang.org/protobuf/types/known/timestamppb" @@ -23,28 +16,6 @@ import ( ) func TestServeExport(t *testing.T) { - query := "message:error" - from := time.Date(2023, time.September, 25, 10, 20, 30, 0, time.UTC) - to := from.Add(time.Second) - - formatReqBody := func(limit int, format exportFormat, fields []string) string { - var sb strings.Builder - sb.WriteString(fmt.Sprintf(`{"query":%q,"from":%q,"to":%q,"offset":0,"limit":%d`, - query, from.Format(time.RFC3339), to.Format(time.RFC3339), limit)) - - if format != "" { - sb.WriteString(fmt.Sprintf(`,"format":%q`, format)) - } - if len(fields) > 0 { - fieldsRaw, err := json.Marshal(fields) - assert.NoError(t, err) - sb.WriteString(fmt.Sprintf(`,"fields":%s`, fieldsRaw)) - } - - sb.WriteString("}") - return sb.String() - } - type mockArgs struct { req *seqapi.ExportRequest err error @@ -53,115 +24,147 @@ func TestServeExport(t *testing.T) { tests := []struct { name string - reqBody string - wantStatus int + req exportRequest + cfg config.SeqAPI + wantErr bool mockArgs *mockArgs - cfg config.SeqAPI }{ { - name: "ok_jsonl", - reqBody: formatReqBody(50, "", nil), + name: "ok_jsonl", + req: exportRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 50, + Offset: 0, + }, + cfg: config.SeqAPI{ + SeqAPIOptions: &config.SeqAPIOptions{ + MaxExportLimit: 100, + MaxParallelExportRequests: 1, + }, + }, mockArgs: &mockArgs{ req: &seqapi.ExportRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: 50, Offset: 0, }, }, - wantStatus: http.StatusOK, + }, + { + name: "ok_csv", + req: exportRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 50, + Offset: 0, + Format: efCSV, + Fields: []string{"field1", "field2"}, + }, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxExportLimit: 100, MaxParallelExportRequests: 1, }, }, - }, - { - name: "ok_csv", - reqBody: formatReqBody(50, efCSV, []string{"field1", "field2"}), mockArgs: &mockArgs{ req: &seqapi.ExportRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: 50, Offset: 0, Format: seqapi.ExportFormat_EXPORT_FORMAT_CSV, Fields: []string{"field1", "field2"}, }, }, - wantStatus: http.StatusOK, - cfg: config.SeqAPI{ - SeqAPIOptions: &config.SeqAPIOptions{ - MaxExportLimit: 100, - MaxParallelExportRequests: 1, - }, - }, - }, - { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, }, { - name: "err_parallel_limited", - reqBody: formatReqBody(0, "", nil), - wantStatus: http.StatusTooManyRequests, + name: "err_parallel_limited", + req: exportRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 0, + Offset: 0, + }, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxParallelExportRequests: 0, }, }, + wantErr: true, }, { - name: "err_export_limit_max", - reqBody: formatReqBody(10, "", nil), - wantStatus: http.StatusBadRequest, + name: "err_export_limit_max", + req: exportRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 10, + Offset: 0, + }, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxExportLimit: 5, MaxParallelExportRequests: 1, }, }, + wantErr: true, }, { - name: "err_csv_empty_fields", - reqBody: formatReqBody(10, efCSV, nil), - wantStatus: http.StatusBadRequest, + name: "err_csv_empty_fields", + req: exportRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 10, + Offset: 0, + Format: efCSV, + }, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxExportLimit: 100, MaxParallelExportRequests: 1, }, }, + wantErr: true, }, { - name: "err_client", - reqBody: formatReqBody(50, "", nil), - mockArgs: &mockArgs{ - req: &seqapi.ExportRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), - Limit: 50, - Offset: 0, - }, - err: errors.New("client error"), + name: "err_client", + req: exportRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 50, + Offset: 0, }, - wantStatus: http.StatusInternalServerError, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxExportLimit: 100, MaxParallelExportRequests: 1, }, }, + wantErr: true, + mockArgs: &mockArgs{ + req: &seqapi.ExportRequest{ + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), + Limit: 50, + Offset: 0, + }, + err: errSomethingWrong, + }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -169,28 +172,28 @@ func TestServeExport(t *testing.T) { Cfg: tt.cfg, } - req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/export", strings.NewReader(tt.reqBody)) - w := httptest.NewRecorder() - if tt.mockArgs != nil { ctrl := gomock.NewController(t) seqDbMock := mock_seqdb.NewMockClient(ctrl) - cw, _ := httputil.NewChunkedWriter(w) - seqDbMock.EXPECT().Export(gomock.Any(), tt.mockArgs.req, cw). - Return(tt.mockArgs.err).Times(1) + seqDbMock.EXPECT(). + Export(gomock.Any(), tt.mockArgs.req, gomock.Any()). + Return(tt.mockArgs.err). + Times(1) seqData.Mocks.SeqDB = seqDbMock } - s := initTestAPI(seqData) - - s.serveExport(w, req) - - res := w.Result() - defer res.Body.Close() + api := setupTestAPI(seqData) - require.Equal(t, tt.wantStatus, res.StatusCode) + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[exportRequest, struct{}]{ + Method: http.MethodPost, + Target: "/seqapi/v1/export", + Req: tt.req, + Handler: api.serveExport, + WantErr: tt.wantErr, + NoResp: true, + }) }) } } diff --git a/internal/api/seqapi/v1/http/fetch_async_search_result_test.go b/internal/api/seqapi/v1/http/fetch_async_search_result_test.go index c884917..95c1175 100644 --- a/internal/api/seqapi/v1/http/fetch_async_search_result_test.go +++ b/internal/api/seqapi/v1/http/fetch_async_search_result_test.go @@ -2,8 +2,6 @@ package http import ( "net/http" - "net/http/httptest" - "strings" "testing" "time" @@ -13,54 +11,163 @@ import ( "github.com/ozontech/seq-ui/internal/api/httputil" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" - "github.com/ozontech/seq-ui/internal/app/types" - mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" - mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) func TestServeFetchAsyncSearchResult(t *testing.T) { - var ( - mockSearchID = "c9a34cf8-4c66-484e-9cc2-42979d848656" - mockTime = time.Date(2025, 8, 6, 17, 52, 12, 123, time.UTC) - meta = `{"some":"meta"}` - ) - type mockArgs struct { - proxyReq *seqapi.FetchAsyncSearchResultRequest - proxyResp *seqapi.FetchAsyncSearchResultResponse - proxyErr error - - repoResp types.AsyncSearchInfo - repoErr error + req *seqapi.FetchAsyncSearchResultRequest + resp *seqapi.FetchAsyncSearchResultResponse + err error } tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req fetchAsyncSearchResultRequest + want fetchAsyncSearchResultResponse + wantErr bool mockArgs *mockArgs }{ { - name: "ok", - reqBody: `{"search_id":"c9a34cf8-4c66-484e-9cc2-42979d848656","limit":2,"offset":10,"order":"desc"}`, + name: "ok", + req: fetchAsyncSearchResultRequest{ + SearchID: testSearchID, + Limit: 2, + Offset: 10, + Order: oDESC, + }, + want: fetchAsyncSearchResultResponseFromProto(&seqapi.FetchAsyncSearchResultResponse{ + Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, + Request: &seqapi.StartAsyncSearchRequest{ + Retention: durationpb.New(60 * time.Second), + Query: "message:error", + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), + Aggs: []*seqapi.AggregationQuery{ + { + Field: "x", + GroupBy: "level", + Func: seqapi.AggFunc_AGG_FUNC_AVG, + Quantiles: []float64{0.9, 0.5}, + }, + { + Field: "y", + GroupBy: "level", + Func: seqapi.AggFunc_AGG_FUNC_SUM, + Interval: pointerTo("30s"), + }, + }, + Hist: &seqapi.StartAsyncSearchRequest_HistQuery{ + Interval: "1s", + }, + WithDocs: true, + Size: 100, + }, + Response: &seqapi.SearchResponse{ + Events: []*seqapi.Event{ + { + Id: "017a854298010000-850287cfa326a7fc", + Data: map[string]string{ + "level": "3", + "message": "some error", + "x": "2", + }, + Time: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), + }, + { + Id: "017a854298010000-8502fe7f2aa33df3", + Data: map[string]string{ + "level": "2", + "message": "some error 2", + "x": "8", + }, + Time: timestamppb.New(testTimestamp.Add(-2 * time.Minute)), + }, + }, + Total: 2, + Histogram: &seqapi.Histogram{ + Buckets: []*seqapi.Histogram_Bucket{ + { + DocCount: 7, + Key: 1, + }, + { + DocCount: 9, + Key: 2, + }, + }, + }, + Aggregations: []*seqapi.Aggregation{ + { + Buckets: []*seqapi.Aggregation_Bucket{ + { + Key: "3", + Value: pointerTo[float64](2), + NotExists: 0, + Quantiles: []float64{2, 1}, + }, + { + Key: "2", + Value: pointerTo[float64](8), + NotExists: 1, + Quantiles: []float64{7, 4}, + }, + }, + }, + { + Buckets: []*seqapi.Aggregation_Bucket{ + { + Key: "33", + Value: pointerTo[float64](2), + NotExists: 0, + Ts: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + }, + { + Key: "33", + Value: pointerTo[float64](5), + NotExists: 0, + Ts: timestamppb.New(testTimestamp), + }, + { + Key: "22", + Value: pointerTo[float64](8), + NotExists: 1, + Ts: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), + }, + }, + NotExists: 2, + }, + }, + Error: &seqapi.Error{ + Code: seqapi.ErrorCode_ERROR_CODE_UNSPECIFIED, + Message: "some error", + }, + }, + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + Progress: 1, + DiskUsage: 512, + Error: &seqapi.Error{ + Code: seqapi.ErrorCode_ERROR_CODE_NO, + }, + }), mockArgs: &mockArgs{ - proxyReq: &seqapi.FetchAsyncSearchResultRequest{ - SearchId: mockSearchID, + req: &seqapi.FetchAsyncSearchResultRequest{ + SearchId: testSearchID, Limit: 2, Offset: 10, Order: seqapi.Order_ORDER_DESC, }, - proxyResp: &seqapi.FetchAsyncSearchResultResponse{ + resp: &seqapi.FetchAsyncSearchResultResponse{ Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, Request: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), Query: "message:error", - From: timestamppb.New(mockTime.Add(-15 * time.Minute)), - To: timestamppb.New(mockTime), + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), Aggs: []*seqapi.AggregationQuery{ { Field: "x", @@ -90,7 +197,7 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { "message": "some error", "x": "2", }, - Time: timestamppb.New(mockTime.Add(-1 * time.Minute)), + Time: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), }, { Id: "017a854298010000-8502fe7f2aa33df3", @@ -99,7 +206,7 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { "message": "some error 2", "x": "8", }, - Time: timestamppb.New(mockTime.Add(-2 * time.Minute)), + Time: timestamppb.New(testTimestamp.Add(-2 * time.Minute)), }, }, Total: 2, @@ -138,19 +245,19 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Key: "33", Value: pointerTo[float64](2), NotExists: 0, - Ts: timestamppb.New(mockTime.Add(-30 * time.Second)), + Ts: timestamppb.New(testTimestamp.Add(-30 * time.Second)), }, { Key: "33", Value: pointerTo[float64](5), NotExists: 0, - Ts: timestamppb.New(mockTime), + Ts: timestamppb.New(testTimestamp), }, { Key: "22", Value: pointerTo[float64](8), NotExists: 1, - Ts: timestamppb.New(mockTime.Add(-1 * time.Minute)), + Ts: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), }, }, NotExists: 2, @@ -161,39 +268,84 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Message: "some error", }, }, - StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, Error: &seqapi.Error{ Code: seqapi.ErrorCode_ERROR_CODE_NO, }, }, - repoResp: types.AsyncSearchInfo{ - SearchID: mockSearchID, - Meta: meta, - }, }, - wantRespBody: `{"status":"done","request":{"retention":"seconds:60","query":"message:error","from":"2025-08-06T17:37:12.000000123Z","to":"2025-08-06T17:52:12.000000123Z","aggregations":[{"field":"x","group_by":"level","agg_func":"avg","quantiles":[0.9,0.5]},{"field":"y","group_by":"level","agg_func":"sum","interval":"30s"}],"histogram":{"interval":"1s"},"with_docs":true,"size":100},"response":{"events":[{"id":"017a854298010000-850287cfa326a7fc","data":{"level":"3","message":"some error","x":"2"},"time":"2025-08-06T17:51:12.000000123Z"},{"id":"017a854298010000-8502fe7f2aa33df3","data":{"level":"2","message":"some error 2","x":"8"},"time":"2025-08-06T17:50:12.000000123Z"}],"histogram":{"buckets":[{"key":"1","docCount":"7"},{"key":"2","docCount":"9"}]},"aggregations":[{"buckets":[{"key":"3","value":2,"quantiles":[2,1]},{"key":"2","value":8,"not_exists":1,"quantiles":[7,4]}]}],"aggregations_ts":[{"data":{"result":[{"metric":{"level":"33"},"values":[{"timestamp":1754502702,"value":2},{"timestamp":1754502732,"value":5}]},{"metric":{"level":"22"},"values":[{"timestamp":1754502672,"value":8}]}]}}],"total":"2","error":{"code":"ERROR_CODE_UNSPECIFIED","message":"some error"}},"started_at":"2025-08-06T17:51:42.000000123Z","expires_at":"2025-08-06T17:52:42.000000123Z","progress":1,"disk_usage":"512","meta":"{\"some\":\"meta\"}","error":{"code":"ERROR_CODE_NO"}}`, - wantStatus: http.StatusOK, }, { - name: "partial_response", - reqBody: `{"search_id":"c9a34cf8-4c66-484e-9cc2-42979d848656","limit":2,"offset":10,"order":"desc"}`, + name: "partial_response", + req: fetchAsyncSearchResultRequest{ + SearchID: testSearchID, + Limit: 2, + Offset: 10, + Order: oDESC, + }, + want: fetchAsyncSearchResultResponseFromProto(&seqapi.FetchAsyncSearchResultResponse{ + Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, + Request: &seqapi.StartAsyncSearchRequest{ + Retention: durationpb.New(60 * time.Second), + Query: "message:error", + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), + WithDocs: true, + Size: 100, + }, + Response: &seqapi.SearchResponse{ + Events: []*seqapi.Event{ + { + Id: "017a854298010000-850287cfa326a7fc", + Data: map[string]string{ + "level": "3", + "message": "some error", + "x": "2", + }, + Time: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), + }, + { + Id: "017a854298010000-8502fe7f2aa33df3", + Data: map[string]string{ + "level": "2", + "message": "some error 2", + "x": "8", + }, + Time: timestamppb.New(testTimestamp.Add(-2 * time.Minute)), + }, + }, + Total: 2, + Error: &seqapi.Error{ + Code: seqapi.ErrorCode_ERROR_CODE_UNSPECIFIED, + Message: "some error", + }, + }, + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + Progress: 1, + DiskUsage: 512, + Error: &seqapi.Error{ + Code: seqapi.ErrorCode_ERROR_CODE_PARTIAL_RESPONSE, + Message: "partial response", + }, + }), mockArgs: &mockArgs{ - proxyReq: &seqapi.FetchAsyncSearchResultRequest{ - SearchId: mockSearchID, + req: &seqapi.FetchAsyncSearchResultRequest{ + SearchId: testSearchID, Limit: 2, Offset: 10, Order: seqapi.Order_ORDER_DESC, }, - proxyResp: &seqapi.FetchAsyncSearchResultResponse{ + resp: &seqapi.FetchAsyncSearchResultResponse{ Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, Request: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), Query: "message:error", - From: timestamppb.New(mockTime.Add(-15 * time.Minute)), - To: timestamppb.New(mockTime), + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), WithDocs: true, Size: 100, }, @@ -206,7 +358,7 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { "message": "some error", "x": "2", }, - Time: timestamppb.New(mockTime.Add(-1 * time.Minute)), + Time: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), }, { Id: "017a854298010000-8502fe7f2aa33df3", @@ -215,7 +367,7 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { "message": "some error 2", "x": "8", }, - Time: timestamppb.New(mockTime.Add(-2 * time.Minute)), + Time: timestamppb.New(testTimestamp.Add(-2 * time.Minute)), }, }, Total: 2, @@ -224,8 +376,8 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Message: "some error", }, }, - StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, Error: &seqapi.Error{ @@ -233,66 +385,61 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Message: "partial response", }, }, - repoResp: types.AsyncSearchInfo{ - SearchID: mockSearchID, - Meta: meta, - }, }, - wantRespBody: `{"status":"done","request":{"retention":"seconds:60","query":"message:error","from":"2025-08-06T17:37:12.000000123Z","to":"2025-08-06T17:52:12.000000123Z","with_docs":true,"size":100},"response":{"events":[{"id":"017a854298010000-850287cfa326a7fc","data":{"level":"3","message":"some error","x":"2"},"time":"2025-08-06T17:51:12.000000123Z"},{"id":"017a854298010000-8502fe7f2aa33df3","data":{"level":"2","message":"some error 2","x":"8"},"time":"2025-08-06T17:50:12.000000123Z"}],"total":"2","error":{"code":"ERROR_CODE_UNSPECIFIED","message":"some error"}},"started_at":"2025-08-06T17:51:42.000000123Z","expires_at":"2025-08-06T17:52:42.000000123Z","progress":1,"disk_usage":"512","meta":"{\"some\":\"meta\"}","error":{"code":"ERROR_CODE_PARTIAL_RESPONSE","message":"partial response"}}`, - wantStatus: http.StatusOK, - }, - { - name: "err_limit", - reqBody: `{"search_id":"c9a34cf8-4c66-484e-9cc2-42979d848656","limit":-10,"offset":20}`, - wantRespBody: `{"message":"invalid request field: 'limit' must be non-negative"}`, - wantStatus: http.StatusBadRequest, }, { - name: "err_offset", - reqBody: `{"search_id":"c9a34cf8-4c66-484e-9cc2-42979d848656","limit":10,"offset":-20}`, - wantRespBody: `{"message":"invalid request field: 'offset' must be non-negative"}`, - wantStatus: http.StatusBadRequest, + name: "err_limit", + req: fetchAsyncSearchResultRequest{ + SearchID: testSearchID, + Limit: -10, + Offset: 20, + }, + wantErr: true, }, { - name: "invalid id", - reqBody: `{"search_id":"some_invalid_id"}`, - wantRespBody: `{"message":"invalid request field: invalid uuid"}`, - wantStatus: http.StatusBadRequest, + name: "err_offset", + req: fetchAsyncSearchResultRequest{ + SearchID: testSearchID, + Limit: 10, + Offset: -20, + }, + wantErr: true, }, { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, + name: "invalid_id", + req: fetchAsyncSearchResultRequest{ + SearchID: "some invalid id", + }, + wantErr: true, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() + ctrl := gomock.NewController(t) + svcMock := mock_asyncsearches.NewMockService(ctrl) + seqData := test.APITestData{} + seqData.Mocks.AsyncSearchesSvc = svcMock if tt.mockArgs != nil { - ctrl := gomock.NewController(t) - - asyncSearchesRepoMock := mock_repo.NewMockAsyncSearches(ctrl) - asyncSearchesRepoMock.EXPECT().GetAsyncSearchById(gomock.Any(), mockSearchID). - Return(tt.mockArgs.repoResp, tt.mockArgs.repoErr).Times(1) - seqData.Mocks.AsyncSearchesRepo = asyncSearchesRepoMock - - seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().FetchAsyncSearchResult(gomock.Any(), tt.mockArgs.proxyReq). - Return(tt.mockArgs.proxyResp, tt.mockArgs.proxyErr).Times(1) - seqData.Mocks.SeqDB = seqDbMock + svcMock.EXPECT(). + FetchAsyncSearchResult(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - api := initTestAPIWithAsyncSearches(seqData) - req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/async_search/fetch", strings.NewReader(tt.reqBody)) + api := setupTestAPI(seqData) - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveFetchAsyncSearchResult, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[fetchAsyncSearchResultRequest, fetchAsyncSearchResultResponse]{ + Method: http.MethodPost, + Target: "/seqapi/v1/async_search/fetch", + Req: tt.req, + Handler: api.serveFetchAsyncSearchResult, + Want: tt.want, + WantErr: tt.wantErr, }) }) } @@ -300,14 +447,13 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { func TestServeFetchAsyncSearchResult_Disabled(t *testing.T) { seqData := test.APITestData{} - api := initTestAPI(seqData) - req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/async_search/fetch", strings.NewReader("{}")) + api := setupTestAPI(seqData) - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveFetchAsyncSearchResult, - WantRespBody: `{"message":"async searches disabled"}`, - WantStatus: http.StatusBadRequest, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[fetchAsyncSearchResultRequest, struct{}]{ + Method: http.MethodPost, + Target: "/seqapi/v1/async_search/fetch", + Handler: api.serveFetchAsyncSearchResult, + WantErr: true, }) } diff --git a/internal/api/seqapi/v1/http/fields_test.go b/internal/api/seqapi/v1/http/fields_test.go index a22b61f..81bfb7b 100644 --- a/internal/api/seqapi/v1/http/fields_test.go +++ b/internal/api/seqapi/v1/http/fields_test.go @@ -1,16 +1,10 @@ package http import ( - "encoding/json" - "errors" - "io" "net/http" - "net/http/httptest" "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "github.com/ozontech/seq-ui/internal/api/httputil" @@ -31,14 +25,20 @@ func TestServeGetFields(t *testing.T) { cfg config.SeqAPIOptions - wantRespBody string - wantStatus int + want getFieldsResponse + wantErr bool - mockArgs mockArgs + mockArgs *mockArgs }{ { name: "ok", - mockArgs: mockArgs{ + want: getFieldsResponse{ + Fields: fields{ + {Name: "test_name1", Type: "keyword"}, + {Name: "test_name2", Type: "text"}, + }, + }, + mockArgs: &mockArgs{ resp: &seqapi.GetFieldsResponse{ Fields: []*seqapi.Field{ { @@ -52,12 +52,24 @@ func TestServeGetFields(t *testing.T) { }, }, }, - wantRespBody: `{"fields":[{"name":"test_name1","type":"keyword"},{"name":"test_name2","type":"text"}]}`, - wantStatus: http.StatusOK, }, { name: "ok_with_system_and_pinned_fields", - mockArgs: mockArgs{ + want: getFieldsResponse{ + Fields: fields{ + {Name: "test_name1", Type: "keyword"}, + {Name: "test_name2", Type: "text"}, + }, + SystemFields: fields{ + {Name: "field1", Type: "keyword"}, + {Name: "field2", Type: "text"}, + }, + PinnedFields: fields{ + {Name: "field3", Type: "keyword"}, + {Name: "field4", Type: "text"}, + }, + }, + mockArgs: &mockArgs{ resp: &seqapi.GetFieldsResponse{ Fields: []*seqapi.Field{ { @@ -81,19 +93,17 @@ func TestServeGetFields(t *testing.T) { {Name: "field4", Type: "text"}, }, }, - wantRespBody: `{"fields":[{"name":"test_name1","type":"keyword"},{"name":"test_name2","type":"text"}],"system_fields":[{"name":"field1","type":"keyword"},{"name":"field2","type":"text"}],"pinned_fields":[{"name":"field3","type":"keyword"},{"name":"field4","type":"text"}]}`, - wantStatus: http.StatusOK, }, { - name: "err_client", - mockArgs: mockArgs{ - err: errors.New("client error"), + name: "err_client", + wantErr: true, + mockArgs: &mockArgs{ + err: errSomethingWrong, }, - wantStatus: http.StatusInternalServerError, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) @@ -105,31 +115,38 @@ func TestServeGetFields(t *testing.T) { } seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().GetFields(gomock.Any(), gomock.Any()). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) + seqDbMock.EXPECT(). + GetFields(gomock.Any(), gomock.Any()). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) seqData.Mocks.SeqDB = seqDbMock - api := initTestAPI(seqData) - req := httptest.NewRequest(http.MethodGet, "/seqapi/v1/fields", http.NoBody) + api := setupTestAPI(seqData) - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetFields, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getFieldsResponse]{ + Method: http.MethodGet, + Target: "/seqapi/v1/fields", + Handler: api.serveGetFields, + Want: tt.want, + WantErr: tt.wantErr, }) }) } } func TestServeGetFieldsCached(t *testing.T) { - type TestCase struct { - resp *seqapi.GetFieldsResponse - wantRespBody string - } + var ( + ttl = 5 * time.Millisecond + ) + + tests := []struct { + name string - tests := []TestCase{ + resp *seqapi.GetFieldsResponse + want getFieldsResponse + }{ { + name: "ok", resp: &seqapi.GetFieldsResponse{ Fields: []*seqapi.Field{ { @@ -142,9 +159,15 @@ func TestServeGetFieldsCached(t *testing.T) { }, }, }, - wantRespBody: `{"fields":[{"name":"n1","type":"keyword"},{"name":"n2","type":"text"}]}`, + want: getFieldsResponse{ + Fields: fields{ + {Name: "n1", Type: "keyword"}, + {Name: "n2", Type: "text"}, + }, + }, }, { + name: "another_ok", resp: &seqapi.GetFieldsResponse{ Fields: []*seqapi.Field{ { @@ -153,51 +176,55 @@ func TestServeGetFieldsCached(t *testing.T) { }, }, }, - wantRespBody: `{"fields":[{"name":"qwe","type":"keyword"}]}`, + want: getFieldsResponse{ + Fields: fields{ + {Name: "qwe", Type: "keyword"}, + }, + }, }, } - ctrl := gomock.NewController(t) - seqDbMock := mock_seqdb.NewMockClient(ctrl) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() - for _, testCase := range tests { - seqDbMock.EXPECT().GetFields(gomock.Any(), gomock.Any()). - Return(testCase.resp, nil).Times(1) - } + ctrl := gomock.NewController(t) + seqDbMock := mock_seqdb.NewMockClient(ctrl) - const ttl = 20 * time.Millisecond + seqDbMock.EXPECT(). + GetFields(gomock.Any(), gomock.Any()). + Return(tt.resp, nil). + Times(1) - seqData := test.APITestData{ - Cfg: config.SeqAPI{ - SeqAPIOptions: &config.SeqAPIOptions{ - FieldsCacheTTL: ttl, - }, - }, - Mocks: test.Mocks{ - SeqDB: seqDbMock, - }, - } + seqData := test.APITestData{ + Cfg: config.SeqAPI{ + SeqAPIOptions: &config.SeqAPIOptions{ + FieldsCacheTTL: ttl, + }, + }, + Mocks: test.Mocks{ + SeqDB: seqDbMock, + }, + } - api := initTestAPI(seqData) + api := setupTestAPI(seqData) - for _, testCase := range tests { - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: httptest.NewRequest(http.MethodGet, "/seqapi/v1/fields", http.NoBody), - Handler: api.serveGetFields, - WantRespBody: testCase.wantRespBody, - WantStatus: http.StatusOK, - }) + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getFieldsResponse]{ + Method: http.MethodGet, + Target: "/seqapi/v1/fields", + Handler: api.serveGetFields, + Want: tt.want, + }) - time.Sleep(ttl / 2) + time.Sleep(ttl / 2) - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: httptest.NewRequest(http.MethodGet, "/seqapi/v1/fields", http.NoBody), - Handler: api.serveGetFields, - WantRespBody: testCase.wantRespBody, - WantStatus: http.StatusOK, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getFieldsResponse]{ + Method: http.MethodGet, + Target: "/seqapi/v1/fields", + Handler: api.serveGetFields, + Want: tt.want, + }) }) - - time.Sleep(ttl) } } @@ -205,8 +232,8 @@ func TestServeGetPinnedFields(t *testing.T) { tests := []struct { name string - fields []config.Field - wantRespBody string + fields []config.Field + want getFieldsResponse }{ { name: "ok", @@ -214,15 +241,16 @@ func TestServeGetPinnedFields(t *testing.T) { {Name: "field1", Type: "keyword"}, {Name: "field2", Type: "text"}, }, - wantRespBody: `{"fields":[{"name":"field1","type":"keyword"},{"name":"field2","type":"text"}]}`, - }, - { - name: "empty", - wantRespBody: `{"fields":[]}`, + want: getFieldsResponse{ + Fields: fields{ + {Name: "field1", Type: "keyword"}, + {Name: "field2", Type: "text"}, + }, + }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -234,27 +262,14 @@ func TestServeGetPinnedFields(t *testing.T) { }, } - api := initTestAPI(seqData) - req := httptest.NewRequest(http.MethodGet, "/seqapi/v1/fields/pinned", http.NoBody) - w := httptest.NewRecorder() - - api.serveGetPinnedFields(w, req) - - resp := w.Result() - defer resp.Body.Close() - - respBody, err := io.ReadAll(resp.Body) - assert.NoError(t, err) + api := setupTestAPI(seqData) - var gfr getFieldsResponse - err = json.Unmarshal(respBody, &gfr) - assert.NoError(t, err) - - require.Equal(t, len(tt.fields), len(gfr.Fields)) - for i, f := range gfr.Fields { - require.Equal(t, tt.fields[i].Name, f.Name) - require.Equal(t, tt.fields[i].Type, f.Type) - } + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getFieldsResponse]{ + Method: http.MethodGet, + Target: "/seqapi/v1/fields/pinned", + Handler: api.serveGetPinnedFields, + Want: tt.want, + }) }) } } diff --git a/internal/api/seqapi/v1/http/get_async_searches_list_test.go b/internal/api/seqapi/v1/http/get_async_searches_list_test.go index 1811ae6..5934a98 100644 --- a/internal/api/seqapi/v1/http/get_async_searches_list_test.go +++ b/internal/api/seqapi/v1/http/get_async_searches_list_test.go @@ -2,7 +2,6 @@ package http import ( "net/http" - "net/http/httptest" "strings" "testing" "time" @@ -13,68 +12,106 @@ import ( "github.com/ozontech/seq-ui/internal/api/httputil" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" - "github.com/ozontech/seq-ui/internal/app/config" - "github.com/ozontech/seq-ui/internal/app/types" - mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" - mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) func TestServeGetAsyncSearchesList(t *testing.T) { - var ( - mockSearchID1 = "c9a34cf8-4c66-484e-9cc2-42979d848656" - mockSearchID2 = "9e4c068e-d4f4-4a5d-be27-a6524a70d70d" - mockUserName1 = "some_user_1" - mockUserName2 = "some_user_2" - mockProfileID1 int64 = 1 - mockProfileID2 int64 = 1 - errorMsg = "some error" - tooLongQuery = strings.Repeat("message:error and level:3", 41) - TruncatedQuery = strings.Repeat("message:error and level:3", 40) - mockTime = time.Date(2025, 8, 6, 17, 52, 12, 123, time.UTC) - ) + statusDone := asyncSearchStatus("done") + mockUserName1 := "some_user_1" + mockUserName2 := "some_user_2" + tooLongQuery := strings.Repeat("message:error and level:3", 41) + errorMsg := "some error" + mockSearchID2 := "9e4c068e-d4f4-4a5d-be27-a6524a70d70d" type mockArgs struct { - searchIDs []string - proxyReq *seqapi.GetAsyncSearchesListRequest - proxyResp *seqapi.GetAsyncSearchesListResponse - proxyErr error - - repoReq types.GetAsyncSearchesListRequest - repoResp []types.AsyncSearchInfo - repoErr error + req *seqapi.GetAsyncSearchesListRequest + resp *seqapi.GetAsyncSearchesListResponse + err error } tests := []struct { name string - reqBody string - cfg config.Handlers - wantRespBody string - wantStatus int + req getAsyncSearchesListRequest + want getAsyncSearchesListResponse + wantErr bool mockArgs *mockArgs }{ { - name: "ok_no_filters", - reqBody: `{}`, + name: "ok_no_filters", + req: getAsyncSearchesListRequest{}, + want: getAsyncSearchesListResponseFromProto(&seqapi.GetAsyncSearchesListResponse{ + Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ + { + SearchId: testSearchID, + Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, + Request: &seqapi.StartAsyncSearchRequest{ + Retention: durationpb.New(60 * time.Second), + Query: "message:error", + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), + WithDocs: true, + Size: 100, + }, + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + Progress: 1, + DiskUsage: 512, + OwnerName: mockUserName1, + Error: &errorMsg, + }, + { + SearchId: mockSearchID2, + Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_CANCELED, + Request: &seqapi.StartAsyncSearchRequest{ + Retention: durationpb.New(360 * time.Second), + Query: "message:error and level:3", + From: timestamppb.New(testTimestamp.Add(-1 * time.Hour)), + To: timestamppb.New(testTimestamp), + Aggs: []*seqapi.AggregationQuery{ + { + Field: "x", + GroupBy: "level", + Func: seqapi.AggFunc_AGG_FUNC_AVG, + Interval: pointerTo("30s"), + }, + }, + Hist: &seqapi.StartAsyncSearchRequest_HistQuery{ + Interval: "1s", + }, + WithDocs: false, + }, + StartedAt: timestamppb.New(testTimestamp.Add(-60 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(300 * time.Second)), + CanceledAt: timestamppb.New(testTimestamp), + Progress: 1, + DiskUsage: 256, + OwnerName: mockUserName2, + }, + }, + Error: &seqapi.Error{ + Code: seqapi.ErrorCode_ERROR_CODE_NO, + }, + }), mockArgs: &mockArgs{ - proxyReq: &seqapi.GetAsyncSearchesListRequest{}, - proxyResp: &seqapi.GetAsyncSearchesListResponse{ + req: &seqapi.GetAsyncSearchesListRequest{}, + resp: &seqapi.GetAsyncSearchesListResponse{ Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchId: mockSearchID1, + SearchId: testSearchID, Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, Request: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), Query: "message:error", - From: timestamppb.New(mockTime.Add(-15 * time.Minute)), - To: timestamppb.New(mockTime), + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), WithDocs: true, Size: 100, }, - StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, OwnerName: mockUserName1, @@ -86,8 +123,8 @@ func TestServeGetAsyncSearchesList(t *testing.T) { Request: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(360 * time.Second), Query: "message:error and level:3", - From: timestamppb.New(mockTime.Add(-1 * time.Hour)), - To: timestamppb.New(mockTime), + From: timestamppb.New(testTimestamp.Add(-1 * time.Hour)), + To: timestamppb.New(testTimestamp), Aggs: []*seqapi.AggregationQuery{ { Field: "x", @@ -101,9 +138,9 @@ func TestServeGetAsyncSearchesList(t *testing.T) { }, WithDocs: false, }, - StartedAt: timestamppb.New(mockTime.Add(-60 * time.Second)), - ExpiresAt: timestamppb.New(mockTime.Add(300 * time.Second)), - CanceledAt: timestamppb.New(mockTime), + StartedAt: timestamppb.New(testTimestamp.Add(-60 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(300 * time.Second)), + CanceledAt: timestamppb.New(testTimestamp), Progress: 1, DiskUsage: 256, OwnerName: mockUserName2, @@ -113,49 +150,62 @@ func TestServeGetAsyncSearchesList(t *testing.T) { Code: seqapi.ErrorCode_ERROR_CODE_NO, }, }, - repoReq: types.GetAsyncSearchesListRequest{}, - repoResp: []types.AsyncSearchInfo{ + }, + }, + { + name: "ok_filters", + req: getAsyncSearchesListRequest{ + Status: &statusDone, + Limit: 10, + Offset: 20, + Owner: &mockUserName1, + }, + want: getAsyncSearchesListResponseFromProto(&seqapi.GetAsyncSearchesListResponse{ + Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchID: mockSearchID1, - OwnerID: mockProfileID1, + SearchId: testSearchID, + Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, + Request: &seqapi.StartAsyncSearchRequest{ + Retention: durationpb.New(60 * time.Second), + Query: "message:error", + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), + WithDocs: true, + Size: 100, + }, + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + Progress: 1, + DiskUsage: 512, OwnerName: mockUserName1, }, - { - SearchID: mockSearchID2, - OwnerID: mockProfileID2, - OwnerName: mockUserName2, - }, }, - searchIDs: []string{mockSearchID1, mockSearchID2}, - }, - wantRespBody: `{"searches":[{"search_id":"c9a34cf8-4c66-484e-9cc2-42979d848656","status":"done","request":{"retention":"seconds:60","query":"message:error","from":"2025-08-06T17:37:12.000000123Z","to":"2025-08-06T17:52:12.000000123Z","with_docs":true,"size":100},"started_at":"2025-08-06T17:51:42.000000123Z","expires_at":"2025-08-06T17:52:42.000000123Z","progress":1,"disk_usage":"512","owner_name":"some_user_1","error":"some error"},{"search_id":"9e4c068e-d4f4-4a5d-be27-a6524a70d70d","status":"canceled","request":{"retention":"seconds:360","query":"message:error and level:3","from":"2025-08-06T16:52:12.000000123Z","to":"2025-08-06T17:52:12.000000123Z","aggregations":[{"field":"x","group_by":"level","agg_func":"avg","interval":"30s"}],"histogram":{"interval":"1s"},"with_docs":false,"size":0},"started_at":"2025-08-06T17:51:12.000000123Z","expires_at":"2025-08-06T17:57:12.000000123Z","canceled_at":"2025-08-06T17:52:12.000000123Z","progress":1,"disk_usage":"256","owner_name":"some_user_2"}],"error":{"code":"ERROR_CODE_NO"}}`, - wantStatus: http.StatusOK, - }, - { - name: "ok_filters", - reqBody: `{"limit":10,"offset":20,"status":"done","owner_name":"some_user_1"}`, + Error: &seqapi.Error{ + Code: seqapi.ErrorCode_ERROR_CODE_NO, + }, + }), mockArgs: &mockArgs{ - proxyReq: &seqapi.GetAsyncSearchesListRequest{ + req: &seqapi.GetAsyncSearchesListRequest{ Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE.Enum(), OwnerName: &mockUserName1, Limit: 10, Offset: 20, }, - proxyResp: &seqapi.GetAsyncSearchesListResponse{ + resp: &seqapi.GetAsyncSearchesListResponse{ Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchId: mockSearchID1, + SearchId: testSearchID, Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, Request: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), Query: "message:error", - From: timestamppb.New(mockTime.Add(-15 * time.Minute)), - To: timestamppb.New(mockTime), + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), WithDocs: true, Size: 100, }, - StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, OwnerName: mockUserName1, @@ -165,41 +215,53 @@ func TestServeGetAsyncSearchesList(t *testing.T) { Code: seqapi.ErrorCode_ERROR_CODE_NO, }, }, - repoReq: types.GetAsyncSearchesListRequest{ - Owner: &mockUserName1, - }, - repoResp: []types.AsyncSearchInfo{ + }, + }, + { + name: "partial_response", + req: getAsyncSearchesListRequest{}, + want: getAsyncSearchesListResponseFromProto(&seqapi.GetAsyncSearchesListResponse{ + Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchID: mockSearchID1, - OwnerID: mockProfileID1, + SearchId: testSearchID, + Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, + Request: &seqapi.StartAsyncSearchRequest{ + Retention: durationpb.New(60 * time.Second), + Query: "message:error", + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), + WithDocs: true, + Size: 100, + }, + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + Progress: 1, + DiskUsage: 512, OwnerName: mockUserName1, }, }, - searchIDs: []string{mockSearchID1}, - }, - wantRespBody: `{"searches":[{"search_id":"c9a34cf8-4c66-484e-9cc2-42979d848656","status":"done","request":{"retention":"seconds:60","query":"message:error","from":"2025-08-06T17:37:12.000000123Z","to":"2025-08-06T17:52:12.000000123Z","with_docs":true,"size":100},"started_at":"2025-08-06T17:51:42.000000123Z","expires_at":"2025-08-06T17:52:42.000000123Z","progress":1,"disk_usage":"512","owner_name":"some_user_1"}],"error":{"code":"ERROR_CODE_NO"}}`, - wantStatus: http.StatusOK, - }, - { - name: "partial_response", - reqBody: `{}`, + Error: &seqapi.Error{ + Code: seqapi.ErrorCode_ERROR_CODE_PARTIAL_RESPONSE, + Message: "partial response", + }, + }), mockArgs: &mockArgs{ - proxyReq: &seqapi.GetAsyncSearchesListRequest{}, - proxyResp: &seqapi.GetAsyncSearchesListResponse{ + req: &seqapi.GetAsyncSearchesListRequest{}, + resp: &seqapi.GetAsyncSearchesListResponse{ Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchId: mockSearchID1, + SearchId: testSearchID, Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, Request: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), Query: "message:error", - From: timestamppb.New(mockTime.Add(-15 * time.Minute)), - To: timestamppb.New(mockTime), + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), WithDocs: true, Size: 100, }, - StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, OwnerName: mockUserName1, @@ -210,56 +272,69 @@ func TestServeGetAsyncSearchesList(t *testing.T) { Message: "partial response", }, }, - repoReq: types.GetAsyncSearchesListRequest{}, - repoResp: []types.AsyncSearchInfo{ - { - SearchID: mockSearchID1, - OwnerID: mockProfileID1, - OwnerName: mockUserName1, - }, - }, - searchIDs: []string{mockSearchID1}, }, - wantRespBody: `{"searches":[{"search_id":"c9a34cf8-4c66-484e-9cc2-42979d848656","status":"done","request":{"retention":"seconds:60","query":"message:error","from":"2025-08-06T17:37:12.000000123Z","to":"2025-08-06T17:52:12.000000123Z","with_docs":true,"size":100},"started_at":"2025-08-06T17:51:42.000000123Z","expires_at":"2025-08-06T17:52:42.000000123Z","progress":1,"disk_usage":"512","owner_name":"some_user_1"}],"error":{"code":"ERROR_CODE_PARTIAL_RESPONSE","message":"partial response"}}`, - wantStatus: http.StatusOK, - }, - { - name: "err_limit", - reqBody: `{"limit":-10,"offset":20}`, - wantRespBody: `{"message":"invalid request field: 'limit' must be non-negative"}`, - wantStatus: http.StatusBadRequest, }, { - name: "err_offset", - reqBody: `{"limit":10,"offset":-20}`, - wantRespBody: `{"message":"invalid request field: 'offset' must be non-negative"}`, - wantStatus: http.StatusBadRequest, + name: "err_limit", + req: getAsyncSearchesListRequest{ + Limit: -10, + Offset: 20, + }, + wantErr: true, }, { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, + name: "err_offset", + req: getAsyncSearchesListRequest{ + Limit: 10, + Offset: -20, + }, + wantErr: true, }, { - name: "query_too_long", - reqBody: "{}", + name: "query_too_long", + req: getAsyncSearchesListRequest{}, + want: getAsyncSearchesListResponseFromProto(&seqapi.GetAsyncSearchesListResponse{ + Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ + { + SearchId: testSearchID, + Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, + Request: &seqapi.StartAsyncSearchRequest{ + Retention: durationpb.New(60 * time.Second), + Query: tooLongQuery, + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), + WithDocs: true, + Size: 100, + }, + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + Progress: 1, + DiskUsage: 512, + OwnerName: mockUserName1, + }, + }, + Error: &seqapi.Error{ + Code: seqapi.ErrorCode_ERROR_CODE_PARTIAL_RESPONSE, + Message: "partial response", + }, + }), mockArgs: &mockArgs{ - proxyReq: &seqapi.GetAsyncSearchesListRequest{}, - proxyResp: &seqapi.GetAsyncSearchesListResponse{ + req: &seqapi.GetAsyncSearchesListRequest{}, + resp: &seqapi.GetAsyncSearchesListResponse{ Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchId: mockSearchID1, + SearchId: testSearchID, Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE, Request: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), Query: tooLongQuery, - From: timestamppb.New(mockTime.Add(-15 * time.Minute)), - To: timestamppb.New(mockTime), + From: timestamppb.New(testTimestamp.Add(-15 * time.Minute)), + To: timestamppb.New(testTimestamp), WithDocs: true, Size: 100, }, - StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), + StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, OwnerName: mockUserName1, @@ -270,52 +345,35 @@ func TestServeGetAsyncSearchesList(t *testing.T) { Message: "partial response", }, }, - repoReq: types.GetAsyncSearchesListRequest{}, - repoResp: []types.AsyncSearchInfo{ - { - SearchID: mockSearchID1, - OwnerID: mockProfileID1, - OwnerName: mockUserName1, - }, - }, - searchIDs: []string{mockSearchID1}, }, - wantRespBody: `{"searches":[{"search_id":"c9a34cf8-4c66-484e-9cc2-42979d848656","status":"done","request":{"retention":"seconds:60","query":"` + TruncatedQuery + `...","from":"2025-08-06T17:37:12.000000123Z","to":"2025-08-06T17:52:12.000000123Z","with_docs":true,"size":100},"started_at":"2025-08-06T17:51:42.000000123Z","expires_at":"2025-08-06T17:52:42.000000123Z","progress":1,"disk_usage":"512","owner_name":"some_user_1"}],"error":{"code":"ERROR_CODE_PARTIAL_RESPONSE","message":"partial response"}}`, - wantStatus: http.StatusOK, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - seqData := test.APITestData{ - AsyncCfg: config.AsyncSearch{ - ListQueryLengthLimit: 1000, - }, - } + ctrl := gomock.NewController(t) + svcMock := mock_asyncsearches.NewMockService(ctrl) + seqData := test.APITestData{} + seqData.Mocks.AsyncSearchesSvc = svcMock if tt.mockArgs != nil { - ctrl := gomock.NewController(t) - - asyncSearchesRepoMock := mock_repo.NewMockAsyncSearches(ctrl) - asyncSearchesRepoMock.EXPECT().GetAsyncSearchesList(gomock.Any(), tt.mockArgs.repoReq). - Return(tt.mockArgs.repoResp, tt.mockArgs.repoErr).Times(1) - seqData.Mocks.AsyncSearchesRepo = asyncSearchesRepoMock - - seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().GetAsyncSearchesList(gomock.Any(), tt.mockArgs.proxyReq, tt.mockArgs.searchIDs). - Return(tt.mockArgs.proxyResp, tt.mockArgs.proxyErr).Times(1) - seqData.Mocks.SeqDB = seqDbMock + svcMock.EXPECT(). + GetAsyncSearchesList(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - api := initTestAPIWithAsyncSearches(seqData) - req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/async_search/list", strings.NewReader(tt.reqBody)) + api := setupTestAPI(seqData) - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetAsyncSearchesList, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[getAsyncSearchesListRequest, getAsyncSearchesListResponse]{ + Method: http.MethodPost, + Target: "/seqapi/v1/async_search/list", + Req: tt.req, + Handler: api.serveGetAsyncSearchesList, + Want: tt.want, + WantErr: tt.wantErr, }) }) } @@ -323,13 +381,12 @@ func TestServeGetAsyncSearchesList(t *testing.T) { func TestServeGetAsyncSearchesList_Disabled(t *testing.T) { seqData := test.APITestData{} - api := initTestAPI(seqData) - req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/async_search/list", strings.NewReader("{}")) + api := setupTestAPI(seqData) - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetAsyncSearchesList, - WantRespBody: `{"message":"async searches disabled"}`, - WantStatus: http.StatusBadRequest, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[getAsyncSearchesListRequest, struct{}]{ + Method: http.MethodPost, + Target: "/seqapi/v1/async_search/list", + Handler: api.serveGetAsyncSearchesList, + WantErr: true, }) } diff --git a/internal/api/seqapi/v1/http/get_envs_test.go b/internal/api/seqapi/v1/http/get_envs_test.go index e2f54a6..3ede5d5 100644 --- a/internal/api/seqapi/v1/http/get_envs_test.go +++ b/internal/api/seqapi/v1/http/get_envs_test.go @@ -1,22 +1,19 @@ package http import ( - "encoding/json" "net/http" - "net/http/httptest" "testing" - "github.com/stretchr/testify/require" - + "github.com/ozontech/seq-ui/internal/api/httputil" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" "github.com/ozontech/seq-ui/internal/app/config" ) func TestServeGetEnvs(t *testing.T) { tests := []struct { - name string - cfg config.SeqAPI - wantEnvs []envInfo + name string + cfg config.SeqAPI + want getEnvsResponse }{ { name: "single_env", @@ -29,14 +26,16 @@ func TestServeGetEnvs(t *testing.T) { SeqCLIMaxSearchLimit: 10000, }, }, - wantEnvs: []envInfo{ - { - Env: "", - MaxSearchLimit: 100, - MaxExportLimit: 200, - MaxParallelExportRequests: 2, - MaxAggregationsPerRequest: 5, - SeqCliMaxSearchLimit: 10000, + want: getEnvsResponse{ + []envInfo{ + { + Env: "", + MaxSearchLimit: 100, + MaxExportLimit: 200, + MaxParallelExportRequests: 2, + MaxAggregationsPerRequest: 5, + SeqCliMaxSearchLimit: 10000, + }, }, }, }, @@ -98,53 +97,54 @@ func TestServeGetEnvs(t *testing.T) { }, DefaultEnv: "cluster-10", }, - wantEnvs: []envInfo{ - { - Env: "cluster-10", - MaxSearchLimit: 1000, - MaxExportLimit: 500, - MaxParallelExportRequests: 10, - MaxAggregationsPerRequest: 5, - SeqCliMaxSearchLimit: 2000, - }, - { - Env: "cluster-102", - MaxSearchLimit: 500, - MaxExportLimit: 250, - MaxParallelExportRequests: 5, - MaxAggregationsPerRequest: 3, - SeqCliMaxSearchLimit: 1000, - }, - { - Env: "cluster-220", - MaxSearchLimit: 1000, - MaxExportLimit: 500, - MaxParallelExportRequests: 10, - MaxAggregationsPerRequest: 5, - SeqCliMaxSearchLimit: 2000, - }, - { - Env: "prod", - MaxSearchLimit: 500, - MaxExportLimit: 250, - MaxParallelExportRequests: 5, - MaxAggregationsPerRequest: 3, - SeqCliMaxSearchLimit: 1000, - }, - { - Env: "wyanki", - MaxSearchLimit: 500, - MaxExportLimit: 250, - MaxParallelExportRequests: 5, - MaxAggregationsPerRequest: 3, - SeqCliMaxSearchLimit: 1000, + want: getEnvsResponse{ + []envInfo{ + { + Env: "cluster-10", + MaxSearchLimit: 1000, + MaxExportLimit: 500, + MaxParallelExportRequests: 10, + MaxAggregationsPerRequest: 5, + SeqCliMaxSearchLimit: 2000, + }, + { + Env: "cluster-102", + MaxSearchLimit: 500, + MaxExportLimit: 250, + MaxParallelExportRequests: 5, + MaxAggregationsPerRequest: 3, + SeqCliMaxSearchLimit: 1000, + }, + { + Env: "cluster-220", + MaxSearchLimit: 1000, + MaxExportLimit: 500, + MaxParallelExportRequests: 10, + MaxAggregationsPerRequest: 5, + SeqCliMaxSearchLimit: 2000, + }, + { + Env: "prod", + MaxSearchLimit: 500, + MaxExportLimit: 250, + MaxParallelExportRequests: 5, + MaxAggregationsPerRequest: 3, + SeqCliMaxSearchLimit: 1000, + }, + { + Env: "wyanki", + MaxSearchLimit: 500, + MaxExportLimit: 250, + MaxParallelExportRequests: 5, + MaxAggregationsPerRequest: 3, + SeqCliMaxSearchLimit: 1000, + }, }, }, }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -152,16 +152,14 @@ func TestServeGetEnvs(t *testing.T) { Cfg: tt.cfg, } - api := initTestAPI(seqData) - - req := httptest.NewRequest(http.MethodGet, "/seqapi/v1/envs", http.NoBody) - w := httptest.NewRecorder() - api.serveGetEnvs(w, req) + api := setupTestAPI(seqData) - var response getEnvsResponse - err := json.NewDecoder(w.Body).Decode(&response) - require.NoError(t, err, "failed to decode response") - require.ElementsMatch(t, tt.wantEnvs, response.Envs, "Returned envs do not match expected") + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getEnvsResponse]{ + Method: http.MethodGet, + Target: "/seqapi/v1/envs", + Handler: api.serveGetEnvs, + Want: tt.want, + }) }) } } diff --git a/internal/api/seqapi/v1/http/histogram_test.go b/internal/api/seqapi/v1/http/histogram_test.go index ea5be57..e0efedb 100644 --- a/internal/api/seqapi/v1/http/histogram_test.go +++ b/internal/api/seqapi/v1/http/histogram_test.go @@ -2,10 +2,7 @@ package http import ( "errors" - "fmt" "net/http" - "net/http/httptest" - "strings" "testing" "time" @@ -19,15 +16,6 @@ import ( ) func TestServeGetHistogram(t *testing.T) { - query := "message:error" - from := time.Date(2023, time.September, 25, 10, 20, 30, 0, time.UTC) - to := from.Add(time.Second) - - formatReqBody := func(interval string) string { - return fmt.Sprintf(`{"query":%q,"from":%q,"to":%q,"interval":%q}`, - query, from.Format(time.RFC3339), to.Format(time.RFC3339), interval) - } - type mockArgs struct { req *seqapi.GetHistogramRequest resp *seqapi.GetHistogramResponse @@ -37,20 +25,35 @@ func TestServeGetHistogram(t *testing.T) { tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req getHistogramRequest + want getHistogramResponse + wantErr bool mockArgs *mockArgs }{ { - name: "ok", - reqBody: formatReqBody("5s"), + name: "ok", + req: getHistogramRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Interval: "5s", + }, + want: getHistogramResponse{ + Histogram: histogram{ + Buckets: histogramBuckets{ + {Key: "0", DocCount: "1"}, + {Key: "100", DocCount: "2"}, + }, + }, + Error: apiError{Code: aecNo}, + PartialResponse: false, + }, mockArgs: &mockArgs{ req: &seqapi.GetHistogramRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Interval: "5s", }, resp: &seqapi.GetHistogramResponse{ @@ -60,17 +63,27 @@ func TestServeGetHistogram(t *testing.T) { }, }, }, - wantRespBody: `{"histogram":{"buckets":[{"key":"0","docCount":"1"},{"key":"100","docCount":"2"}]},"error":{"code":"ERROR_CODE_NO"},"partialResponse":false}`, - wantStatus: http.StatusOK, }, { - name: "err_partial_response", - reqBody: formatReqBody("10s"), + name: "err_partial_response", + req: getHistogramRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Interval: "10s", + }, + want: getHistogramResponse{ + Histogram: histogram{ + Buckets: histogramBuckets{}, + }, + Error: apiError{Code: aecPartialResponse, Message: "partial response"}, + PartialResponse: true, + }, mockArgs: &mockArgs{ req: &seqapi.GetHistogramRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Interval: "10s", }, resp: &seqapi.GetHistogramResponse{ @@ -81,31 +94,29 @@ func TestServeGetHistogram(t *testing.T) { PartialResponse: true, }, }, - wantRespBody: `{"histogram":{"buckets":[]},"error":{"code":"ERROR_CODE_PARTIAL_RESPONSE","message":"partial response"},"partialResponse":true}`, - wantStatus: http.StatusOK, - }, - { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, }, { - name: "err_client", - reqBody: formatReqBody("20s"), + name: "err_client", + req: getHistogramRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Interval: "20s", + }, + wantErr: true, mockArgs: &mockArgs{ req: &seqapi.GetHistogramRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Interval: "20s", }, err: errors.New("client error"), }, - wantStatus: http.StatusInternalServerError, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -113,22 +124,25 @@ func TestServeGetHistogram(t *testing.T) { if tt.mockArgs != nil { ctrl := gomock.NewController(t) - seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().GetHistogram(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) + + seqDbMock.EXPECT(). + GetHistogram(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) seqData.Mocks.SeqDB = seqDbMock } - api := initTestAPI(seqData) - req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/histogram", strings.NewReader(tt.reqBody)) + api := setupTestAPI(seqData) - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetHistogram, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[getHistogramRequest, getHistogramResponse]{ + Method: http.MethodPost, + Target: "/seqapi/v1/histogram", + Req: tt.req, + Handler: api.serveGetHistogram, + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/seqapi/v1/http/limits_test.go b/internal/api/seqapi/v1/http/limits_test.go index 28d3748..cf339e4 100644 --- a/internal/api/seqapi/v1/http/limits_test.go +++ b/internal/api/seqapi/v1/http/limits_test.go @@ -2,7 +2,6 @@ package http import ( "net/http" - "net/http/httptest" "testing" "github.com/ozontech/seq-ui/internal/api/httputil" @@ -12,10 +11,11 @@ import ( func TestServeGetLimits(t *testing.T) { tests := []struct { - name string - env string - cfg config.SeqAPI - wantRespBody string + name string + + env string + cfg config.SeqAPI + want getLimitsResponse }{ { name: "ok", @@ -29,16 +29,22 @@ func TestServeGetLimits(t *testing.T) { SeqCLIMaxSearchLimit: 10000, }, }, - wantRespBody: `{"maxSearchLimit":100,"maxExportLimit":200,"maxParallelExportRequests":2,"maxAggregationsPerRequest":5,"seqCliMaxSearchLimit":10000}`, + want: getLimitsResponse{ + MaxSearchLimit: 100, + MaxExportLimit: 200, + MaxParallelExportRequests: 2, + MaxAggregationsPerRequest: 5, + SeqCliMaxSearchLimit: 10000, + }, }, { - name: "empty", - env: "default", - wantRespBody: `{"maxSearchLimit":0,"maxExportLimit":0,"maxParallelExportRequests":0,"maxAggregationsPerRequest":0,"seqCliMaxSearchLimit":0}`, + name: "empty", + env: "default", + want: getLimitsResponse{}, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -46,14 +52,13 @@ func TestServeGetLimits(t *testing.T) { Cfg: tt.cfg, } - api := initTestAPI(seqData) - req := httptest.NewRequest(http.MethodGet, "/seqapi/v1/limits", http.NoBody) + api := setupTestAPI(seqData) - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetLimits, - WantRespBody: tt.wantRespBody, - WantStatus: http.StatusOK, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getLimitsResponse]{ + Method: http.MethodGet, + Target: "/seqapi/v1/limits", + Handler: api.serveGetLimits, + Want: tt.want, }) }) } diff --git a/internal/api/seqapi/v1/http/logs_lifespan_test.go b/internal/api/seqapi/v1/http/logs_lifespan_test.go index a3d4401..0ed4b5c 100644 --- a/internal/api/seqapi/v1/http/logs_lifespan_test.go +++ b/internal/api/seqapi/v1/http/logs_lifespan_test.go @@ -1,9 +1,7 @@ package http import ( - "errors" "net/http" - "net/http/httptest" "strconv" "testing" "time" @@ -22,13 +20,11 @@ import ( ) func TestServeGetLogsLifespan(t *testing.T) { - const ( - cacheKey = "logs_lifespan" - cacheTTL = 1 * time.Minute - - result = 10 * time.Hour - resultStr = "36000" // 10(h) * 60(min/h) * 60(sec/min) - resultRespBody = `{"lifespan":36000}` + var ( + resultStr = "36000" // 10(h) * 60(min/h) * 60(sec/min) + cacheKey = "logs_lifespan" + result = 10 * time.Hour + cacheTTL = time.Minute ) unparsable := func(s string) bool { @@ -36,8 +32,6 @@ func TestServeGetLogsLifespan(t *testing.T) { return err != nil } - oldestStorageTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) - tests := []struct { name string @@ -47,16 +41,15 @@ func TestServeGetLogsLifespan(t *testing.T) { clientResp *seqapi.StatusResponse clientErr error - wantStatus int - wantRespBody string + wantErr bool + want getLogsLifespanResponse }{ { name: "ok_cached", getOp: test.CacheMockArgs{ Value: resultStr, }, - wantStatus: http.StatusOK, - wantRespBody: resultRespBody, + want: getLogsLifespanResponse{Lifespan: 36000}, }, { name: "ok_cached_unparsable", @@ -67,10 +60,9 @@ func TestServeGetLogsLifespan(t *testing.T) { Value: resultStr, }, clientResp: &seqapi.StatusResponse{ - OldestStorageTime: timestamppb.New(oldestStorageTime), + OldestStorageTime: timestamppb.New(testTimestamp), }, - wantStatus: http.StatusOK, - wantRespBody: resultRespBody, + want: getLogsLifespanResponse{Lifespan: 36000}, }, { name: "ok_no_cached", @@ -81,18 +73,17 @@ func TestServeGetLogsLifespan(t *testing.T) { Value: resultStr, }, clientResp: &seqapi.StatusResponse{ - OldestStorageTime: timestamppb.New(oldestStorageTime), + OldestStorageTime: timestamppb.New(testTimestamp), }, - wantStatus: http.StatusOK, - wantRespBody: resultRespBody, + want: getLogsLifespanResponse{Lifespan: 36000}, }, { name: "err_client", getOp: test.CacheMockArgs{ Err: cache.ErrNotFound, }, - clientErr: errors.New("network error"), - wantStatus: http.StatusInternalServerError, + clientErr: errSomethingWrong, + wantErr: true, }, { name: "err_nil_oldest_storage_time", @@ -102,11 +93,11 @@ func TestServeGetLogsLifespan(t *testing.T) { clientResp: &seqapi.StatusResponse{ OldestStorageTime: nil, }, - wantStatus: http.StatusInternalServerError, + wantErr: true, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -118,37 +109,43 @@ func TestServeGetLogsLifespan(t *testing.T) { }, }, } - ctrl := gomock.NewController(t) + ctrl := gomock.NewController(t) cacheMock := mock_cache.NewMockCache(ctrl) - cacheMock.EXPECT().Get(gomock.Any(), cacheKey). - Return(tt.getOp.Value, tt.getOp.Err).Times(1) + + cacheMock.EXPECT(). + Get(gomock.Any(), cacheKey). + Return(tt.getOp.Value, tt.getOp.Err). + Times(1) seqData.Mocks.Cache = cacheMock if tt.getOp.Err != nil || unparsable(tt.getOp.Value) { seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().Status(gomock.Any(), gomock.Any()). - Return(proto.Clone(tt.clientResp), tt.clientErr).Times(1) + seqDbMock.EXPECT(). + Status(gomock.Any(), gomock.Any()). + Return(proto.Clone(tt.clientResp), tt.clientErr). + Times(1) seqData.Mocks.SeqDB = seqDbMock if tt.clientErr == nil && tt.clientResp.OldestStorageTime != nil { - cacheMock.EXPECT().SetWithTTL(gomock.Any(), cacheKey, tt.setOp.Value, cacheTTL). - Return(tt.setOp.Err).Times(1) + cacheMock.EXPECT(). + SetWithTTL(gomock.Any(), cacheKey, tt.setOp.Value, cacheTTL). + Return(tt.setOp.Err). + Times(1) } } - api := initTestAPI(seqData) + api := setupTestAPI(seqData) api.nowFn = func() time.Time { - return oldestStorageTime.Add(result) + return testTimestamp.Add(result) } - req := httptest.NewRequest(http.MethodGet, "/seqapi/v1/logs_lifespan", http.NoBody) - - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetLogsLifespan, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getLogsLifespanResponse]{ + Method: http.MethodGet, + Target: "/seqapi/v1/logs_lifespan", + Handler: api.serveGetLogsLifespan, + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/seqapi/v1/http/search_test.go b/internal/api/seqapi/v1/http/search_test.go index 1dc3932..c22272f 100644 --- a/internal/api/seqapi/v1/http/search_test.go +++ b/internal/api/seqapi/v1/http/search_test.go @@ -1,16 +1,10 @@ package http import ( - "encoding/json" - "errors" - "fmt" "net/http" - "net/http/httptest" - "strings" "testing" "time" - "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "google.golang.org/protobuf/types/known/timestamppb" @@ -23,34 +17,9 @@ import ( ) func TestServeSearch(t *testing.T) { - query := "message:error" - from := time.Date(2023, time.September, 25, 10, 20, 30, 0, time.UTC) - to := from.Add(time.Second) - eventTime := from.Add(time.Millisecond) // 2023-09-25T10:20:30.001Z - - formatReqBody := func(limit, offset int, withTotal bool, histInterval string, aggQueries aggregationQueries, order string) string { - var sb strings.Builder - sb.WriteString(fmt.Sprintf(`{"query":%q,"from":%q,"to":%q,"limit":%d,"offset":%d`, - query, from.Format(time.RFC3339), to.Format(time.RFC3339), limit, offset)) - - if withTotal { - sb.WriteString(fmt.Sprintf(`,"withTotal":%v`, withTotal)) - } - if histInterval != "" { - sb.WriteString(fmt.Sprintf(`,"histogram":{"interval":%q}`, histInterval)) - } - if len(aggQueries) > 0 { - aggQueriesRaw, err := json.Marshal(aggQueries) - assert.NoError(t, err) - sb.WriteString(fmt.Sprintf(`,"aggregations":%s`, aggQueriesRaw)) - } - if order != "" { - sb.WriteString(fmt.Sprintf(`,"order":%q`, order)) - } - - sb.WriteString("}") - return sb.String() - } + var ( + eventTime = testTimestamp.Add(time.Millisecond) + ) type mockArgs struct { req *seqapi.SearchRequest @@ -61,21 +30,36 @@ func TestServeSearch(t *testing.T) { tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req searchRequest + want searchResponse + cfg config.SeqAPI + wantErr bool mockArgs *mockArgs - cfg config.SeqAPI }{ { - name: "ok_simple", - reqBody: formatReqBody(3, 0, false, "", nil, ""), + name: "ok_simple", + req: searchRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 3, + }, + want: searchResponse{ + Events: eventsFromProto(test.MakeEvents(3, eventTime)), + Error: apiError{Code: aecNo}, + PartialResponse: false, + }, + cfg: test.SetCfgDefaults(config.SeqAPI{ + SeqAPIOptions: &config.SeqAPIOptions{ + MaxSearchLimit: 5, + }, + }), mockArgs: &mockArgs{ req: &seqapi.SearchRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: 3, Offset: 0, }, @@ -86,22 +70,32 @@ func TestServeSearch(t *testing.T) { }, }, }, - wantRespBody: `{"events":[{"id":"test1","data":{"field1":"val1"},"time":"2023-09-25T10:20:30.001Z"},{"id":"test2","data":{"field1":"val1","field2":"val2"},"time":"2023-09-25T10:20:30.001Z"},{"id":"test3","data":{"field1":"val1","field2":"val2","field3":"val3"},"time":"2023-09-25T10:20:30.001Z"}],"error":{"code":"ERROR_CODE_NO"},"partialResponse":false}`, - wantStatus: http.StatusOK, + }, + { + name: "ok_with_total", + req: searchRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 3, + WithTotal: true, + }, + want: searchResponse{ + Events: eventsFromProto(test.MakeEvents(3, eventTime)), + Total: "10", + Error: apiError{Code: aecNo}, + PartialResponse: false, + }, cfg: test.SetCfgDefaults(config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxSearchLimit: 5, }, }), - }, - { - name: "ok_with_total", - reqBody: formatReqBody(3, 0, true, "", nil, ""), mockArgs: &mockArgs{ req: &seqapi.SearchRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: 3, Offset: 0, WithTotal: true, @@ -114,22 +108,31 @@ func TestServeSearch(t *testing.T) { }, }, }, - wantRespBody: `{"events":[{"id":"test1","data":{"field1":"val1"},"time":"2023-09-25T10:20:30.001Z"},{"id":"test2","data":{"field1":"val1","field2":"val2"},"time":"2023-09-25T10:20:30.001Z"},{"id":"test3","data":{"field1":"val1","field2":"val2","field3":"val3"},"time":"2023-09-25T10:20:30.001Z"}],"total":"10","error":{"code":"ERROR_CODE_NO"},"partialResponse":false}`, - wantStatus: http.StatusOK, + }, + { + name: "ok_order_asc", + req: searchRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 3, + Order: oASC, + }, + want: searchResponse{ + Events: eventsFromProto(test.MakeEvents(3, eventTime)), + Error: apiError{Code: aecNo}, + PartialResponse: false, + }, cfg: test.SetCfgDefaults(config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxSearchLimit: 5, }, }), - }, - { - name: "ok_order_asc", - reqBody: formatReqBody(3, 0, false, "", nil, string(oASC)), mockArgs: &mockArgs{ req: &seqapi.SearchRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: 3, Offset: 0, Order: seqapi.Order_ORDER_ASC, @@ -141,22 +144,34 @@ func TestServeSearch(t *testing.T) { }, }, }, - wantRespBody: `{"events":[{"id":"test1","data":{"field1":"val1"},"time":"2023-09-25T10:20:30.001Z"},{"id":"test2","data":{"field1":"val1","field2":"val2"},"time":"2023-09-25T10:20:30.001Z"},{"id":"test3","data":{"field1":"val1","field2":"val2","field3":"val3"},"time":"2023-09-25T10:20:30.001Z"}],"error":{"code":"ERROR_CODE_NO"},"partialResponse":false}`, - wantStatus: http.StatusOK, + }, + { + name: "ok_with_hist", + req: searchRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 3, + Histogram: struct { + Interval string "json:\"interval\"" + }{Interval: "5s"}, + }, + want: searchResponse{ + Events: eventsFromProto(test.MakeEvents(3, eventTime)), + Histogram: histogramFromProto(test.MakeHistogram(2), false), + Error: apiError{Code: aecNo}, + PartialResponse: false, + }, cfg: test.SetCfgDefaults(config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxSearchLimit: 5, }, }), - }, - { - name: "ok_with_hist", - reqBody: formatReqBody(3, 0, false, "5s", nil, ""), mockArgs: &mockArgs{ req: &seqapi.SearchRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: 3, Offset: 0, Histogram: &seqapi.SearchRequest_Histogram{ @@ -171,28 +186,40 @@ func TestServeSearch(t *testing.T) { }, }, }, - wantRespBody: `{"events":[{"id":"test1","data":{"field1":"val1"},"time":"2023-09-25T10:20:30.001Z"},{"id":"test2","data":{"field1":"val1","field2":"val2"},"time":"2023-09-25T10:20:30.001Z"},{"id":"test3","data":{"field1":"val1","field2":"val2","field3":"val3"},"time":"2023-09-25T10:20:30.001Z"}],"histogram":{"buckets":[{"key":"0","docCount":"1"},{"key":"100","docCount":"2"}]},"error":{"code":"ERROR_CODE_NO"},"partialResponse":false}`, - wantStatus: http.StatusOK, + }, + { + name: "ok_with_aggs", + req: searchRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 3, + Aggregations: aggregationQueries{ + { + Field: "test1", + GroupBy: "service", + Func: afAvg, + }, + }, + }, + want: searchResponse{ + Events: eventsFromProto(test.MakeEvents(3, eventTime)), + Aggregations: aggregationsFromProto(test.MakeAggregations(2, 3, &test.MakeAggOpts{ + NotExists: 5, + }), false), + Error: apiError{Code: aecNo}, + PartialResponse: false, + }, cfg: test.SetCfgDefaults(config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxSearchLimit: 5, }, }), - }, - { - name: "ok_with_aggs", - reqBody: formatReqBody(3, 0, false, "", aggregationQueries{ - { - Field: "test1", - GroupBy: "service", - Func: afAvg, - }, - }, ""), mockArgs: &mockArgs{ req: &seqapi.SearchRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: 3, Offset: 0, Aggregations: []*seqapi.AggregationQuery{ @@ -213,22 +240,30 @@ func TestServeSearch(t *testing.T) { }, }, }, - wantRespBody: `{"events":[{"id":"test1","data":{"field1":"val1"},"time":"2023-09-25T10:20:30.001Z"},{"id":"test2","data":{"field1":"val1","field2":"val2"},"time":"2023-09-25T10:20:30.001Z"},{"id":"test3","data":{"field1":"val1","field2":"val2","field3":"val3"},"time":"2023-09-25T10:20:30.001Z"}],"aggregations":[{"buckets":[{"key":"test1","value":1,"not_exists":5},{"key":"test2","value":2,"not_exists":5},{"key":"test3","value":3,"not_exists":5}]},{"buckets":[{"key":"test1","value":1,"not_exists":5},{"key":"test2","value":2,"not_exists":5},{"key":"test3","value":3,"not_exists":5}]}],"error":{"code":"ERROR_CODE_NO"},"partialResponse":false}`, - wantStatus: http.StatusOK, + }, + { + name: "ok_empty_events", + req: searchRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 3, + }, + want: searchResponse{ + Events: eventsFromProto(test.MakeEvents(0, eventTime)), + Error: apiError{Code: aecNo}, + PartialResponse: false, + }, cfg: test.SetCfgDefaults(config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxSearchLimit: 5, }, }), - }, - { - name: "ok_empty_events", - reqBody: formatReqBody(3, 0, false, "", nil, ""), mockArgs: &mockArgs{ req: &seqapi.SearchRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: 3, Offset: 0, }, @@ -239,22 +274,30 @@ func TestServeSearch(t *testing.T) { }, }, }, - wantRespBody: `{"events":[],"error":{"code":"ERROR_CODE_NO"},"partialResponse":false}`, - wantStatus: http.StatusOK, + }, + { + name: "err_partial_response", + req: searchRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 3, + }, + want: searchResponse{ + Events: eventsFromProto(test.MakeEvents(1, eventTime)), + Error: apiError{Code: aecPartialResponse, Message: "partial response"}, + PartialResponse: true, + }, cfg: test.SetCfgDefaults(config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxSearchLimit: 5, }, }), - }, - { - name: "err_partial_response", - reqBody: formatReqBody(3, 0, false, "", nil, ""), mockArgs: &mockArgs{ req: &seqapi.SearchRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: 3, Offset: 0, }, @@ -267,63 +310,92 @@ func TestServeSearch(t *testing.T) { PartialResponse: true, }, }, - wantRespBody: `{"events":[{"id":"test1","data":{"field1":"val1"},"time":"2023-09-25T10:20:30.001Z"}],"error":{"code":"ERROR_CODE_PARTIAL_RESPONSE","message":"partial response"},"partialResponse":true}`, - wantStatus: http.StatusOK, - cfg: test.SetCfgDefaults(config.SeqAPI{ - SeqAPIOptions: &config.SeqAPIOptions{ - MaxSearchLimit: 5, - }, - }), - }, - { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, }, { - name: "err_search_limit_zero", - reqBody: formatReqBody(0, 0, false, "", nil, ""), - wantStatus: http.StatusBadRequest, + name: "err_search_limit_zero", + req: searchRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 0, + }, + wantErr: true, }, { - name: "err_search_limit_max", - reqBody: formatReqBody(10, 0, false, "", nil, ""), - wantStatus: http.StatusBadRequest, + name: "err_search_limit_max", + req: searchRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 10, + }, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxSearchLimit: 5, }, }, + wantErr: true, }, { - name: "err_aggs_limit_max", - reqBody: formatReqBody(3, 0, false, "", aggregationQueries{{}, {}, {}}, ""), - wantStatus: http.StatusBadRequest, + name: "err_aggs_limit_max", + req: searchRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 3, + Aggregations: aggregationQueries{{}, {}, {}}, + }, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxSearchLimit: 5, MaxAggregationsPerRequest: 2, }, }, - }, { - name: "err_offset_too_high", - reqBody: formatReqBody(3, 11, false, "", nil, ""), - wantStatus: http.StatusBadRequest, + wantErr: true, + }, + { + name: "err_offset_too_high", + req: searchRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 3, + Offset: 11, + }, cfg: test.SetCfgDefaults(config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxSearchLimit: 5, MaxSearchOffsetLimit: 10, }, }), + wantErr: true, }, { - name: "err_total_too_high", - reqBody: formatReqBody(3, 0, true, "", nil, ""), + name: "err_total_too_high", + req: searchRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 3, + WithTotal: true, + }, + want: searchResponse{ + Events: eventsFromProto(test.MakeEvents(1, eventTime)), + Total: "11", + Error: apiError{Code: aecQueryTooHeavy, Message: api_error.ErrQueryTooHeavy.Error()}, + PartialResponse: false, + }, + cfg: test.SetCfgDefaults(config.SeqAPI{ + SeqAPIOptions: &config.SeqAPIOptions{ + MaxSearchLimit: 5, + MaxSearchOffsetLimit: 10, + }, + }), mockArgs: &mockArgs{ req: &seqapi.SearchRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: 3, Offset: 0, WithTotal: true, @@ -337,38 +409,35 @@ func TestServeSearch(t *testing.T) { }, }, }, - wantRespBody: fmt.Sprintf(`{"events":[{"id":"test1","data":{"field1":"val1"},"time":"2023-09-25T10:20:30.001Z"}],"total":"11","error":{"code":"ERROR_CODE_QUERY_TOO_HEAVY","message":%q},"partialResponse":false}`, api_error.ErrQueryTooHeavy.Error()), - wantStatus: http.StatusOK, + }, + { + name: "err_client", + req: searchRequest{ + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + Limit: 3, + }, cfg: test.SetCfgDefaults(config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ - MaxSearchLimit: 5, - MaxSearchOffsetLimit: 10, + MaxSearchLimit: 5, }, }), - }, - { - name: "err_client", - reqBody: formatReqBody(3, 0, false, "", nil, ""), + wantErr: true, mockArgs: &mockArgs{ req: &seqapi.SearchRequest{ - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), Limit: 3, Offset: 0, }, - err: errors.New("client error"), + err: errSomethingWrong, }, - wantStatus: http.StatusInternalServerError, - cfg: test.SetCfgDefaults(config.SeqAPI{ - SeqAPIOptions: &config.SeqAPIOptions{ - MaxSearchLimit: 5, - }, - }), }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -380,20 +449,23 @@ func TestServeSearch(t *testing.T) { ctrl := gomock.NewController(t) seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().Search(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) + seqDbMock.EXPECT(). + Search(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) seqData.Mocks.SeqDB = seqDbMock } - api := initTestAPI(seqData) - req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/search", strings.NewReader(tt.reqBody)) + api := setupTestAPI(seqData) - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveSearch, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[searchRequest, searchResponse]{ + Method: http.MethodPost, + Target: "/seqapi/v1/search", + Req: tt.req, + Handler: api.serveSearch, + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/seqapi/v1/http/start_async_search.go b/internal/api/seqapi/v1/http/start_async_search.go index 0904cc1..1096ec8 100644 --- a/internal/api/seqapi/v1/http/start_async_search.go +++ b/internal/api/seqapi/v1/http/start_async_search.go @@ -103,13 +103,7 @@ func (a *API) serveStartAsyncSearch(w http.ResponseWriter, r *http.Request) { span.SetAttributes(spanAttributes...) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - httputil.ProcessError(wr, err) - return - } - - resp, err := a.asyncSearches.StartAsyncSearch(ctx, profileID, httpReq.toProto(parsedRetention)) + resp, err := a.asyncSearches.StartAsyncSearch(ctx, httpReq.toProto(parsedRetention)) if err != nil { wr.Error(err, http.StatusInternalServerError) return diff --git a/internal/api/seqapi/v1/http/start_async_search_test.go b/internal/api/seqapi/v1/http/start_async_search_test.go index d1ac82d..8541d72 100644 --- a/internal/api/seqapi/v1/http/start_async_search_test.go +++ b/internal/api/seqapi/v1/http/start_async_search_test.go @@ -1,11 +1,7 @@ package http import ( - "context" - "fmt" "net/http" - "net/http/httptest" - "strings" "testing" "time" @@ -15,60 +11,64 @@ import ( "github.com/ozontech/seq-ui/internal/api/httputil" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" - "github.com/ozontech/seq-ui/internal/app/types" - mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" - mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) func TestServeStartAsyncSearch(t *testing.T) { - const ( - mockSearchID = "c9a34cf8-4c66-484e-9cc2-42979d848656" - mockUserName = "some_user" - mockProfileID = 1 - meta = `{"some":"meta"}` + var ( + meta = `{"some":"meta"}` ) - query := "message:error" - from := time.Date(2023, time.September, 25, 10, 20, 30, 0, time.UTC) - to := from.Add(time.Second) - - formatReqBody := func(retention string) string { - return fmt.Sprintf(`{"retention":%q,"query":%q,"from":%q,"to":%q,"with_docs":true,"size":100,"meta":"{\"some\":\"meta\"}","histogram":{"interval":"1s"},"aggregations":[{"field":"v","group_by":"level","agg_func":"avg","quantiles":[0.95],"interval":"30s"}]}`, - retention, query, from.Format(time.RFC3339), to.Format(time.RFC3339)) - } - type mockArgs struct { - proxyReq *seqapi.StartAsyncSearchRequest - proxyResp *seqapi.StartAsyncSearchResponse - proxyErr error - - profilesReq types.GetOrCreateUserProfileRequest - profilesResp types.UserProfile - profilesErr error - - repoReq types.SaveAsyncSearchRequest - repoErr error + req *seqapi.StartAsyncSearchRequest + resp *seqapi.StartAsyncSearchResponse + err error } tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req startAsyncSearchRequest + want startAsyncSearchResponse + wantErr bool mockArgs *mockArgs }{ { - name: "ok", - reqBody: formatReqBody("60s"), + name: "ok", + req: startAsyncSearchRequest{ + Retention: "60s", + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + WithDocs: true, + Size: 100, + Meta: meta, + Histogram: &AsyncSearchRequestHistogram{ + Interval: "1s", + }, + Aggregations: aggregationTsQueries{ + { + aggregationQuery: aggregationQuery{ + Field: "v", + GroupBy: "level", + Func: afAvg, + Quantiles: []float64{0.95}, + }, + Interval: "30s", + }, + }, + }, + want: startAsyncSearchResponse{ + SearchID: testSearchID, + }, mockArgs: &mockArgs{ - proxyReq: &seqapi.StartAsyncSearchRequest{ + req: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), - Query: query, - From: timestamppb.New(from), - To: timestamppb.New(to), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), WithDocs: true, Size: 100, Hist: &seqapi.StartAsyncSearchRequest_HistQuery{ @@ -85,31 +85,64 @@ func TestServeStartAsyncSearch(t *testing.T) { }, Meta: meta, }, - proxyResp: &seqapi.StartAsyncSearchResponse{ - SearchId: mockSearchID, + resp: &seqapi.StartAsyncSearchResponse{ + SearchId: testSearchID, }, - profilesReq: types.GetOrCreateUserProfileRequest{ - UserName: mockUserName, + }, + }, + { + name: "err_svc", + req: startAsyncSearchRequest{ + Retention: "60s", + Query: testQuery, + From: testTimestamp, + To: testTimestamp.Add(time.Second), + WithDocs: true, + Size: 100, + Meta: meta, + Histogram: &AsyncSearchRequestHistogram{ + Interval: "1s", }, - profilesResp: types.UserProfile{ - ID: mockProfileID, - UserName: mockUserName, + Aggregations: aggregationTsQueries{ + { + aggregationQuery: aggregationQuery{ + Field: "v", + GroupBy: "level", + Func: afAvg, + Quantiles: []float64{0.95}, + }, + Interval: "30s", + }, }, - repoReq: types.SaveAsyncSearchRequest{ - SearchID: mockSearchID, - OwnerID: mockProfileID, - Meta: meta, + }, + wantErr: true, + mockArgs: &mockArgs{ + req: &seqapi.StartAsyncSearchRequest{ + Retention: durationpb.New(60 * time.Second), + Query: testQuery, + From: timestamppb.New(testTimestamp), + To: timestamppb.New(testTimestamp.Add(time.Second)), + WithDocs: true, + Size: 100, + Hist: &seqapi.StartAsyncSearchRequest_HistQuery{ + Interval: "1s", + }, + Aggs: []*seqapi.AggregationQuery{ + { + Field: "v", + GroupBy: "level", + Func: seqapi.AggFunc_AGG_FUNC_AVG, + Quantiles: []float64{0.95}, + Interval: pointerTo("30s"), + }, + }, + Meta: meta, }, + err: errSomethingWrong, }, - wantRespBody: `{"search_id":"c9a34cf8-4c66-484e-9cc2-42979d848656"}`, - wantStatus: http.StatusOK, - }, - { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -118,32 +151,25 @@ func TestServeStartAsyncSearch(t *testing.T) { if tt.mockArgs != nil { ctrl := gomock.NewController(t) + svcMock := mock_asyncsearches.NewMockService(ctrl) - seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT().StartAsyncSearch(gomock.Any(), tt.mockArgs.proxyReq). - Return(tt.mockArgs.proxyResp, tt.mockArgs.proxyErr).Times(1) - seqData.Mocks.SeqDB = seqDbMock + svcMock.EXPECT(). + StartAsyncSearch(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) - profilesRepoMock := mock_repo.NewMockUserProfiles(ctrl) - profilesRepoMock.EXPECT().GetOrCreate(gomock.Any(), tt.mockArgs.profilesReq). - Return(tt.mockArgs.profilesResp, tt.mockArgs.profilesErr).Times(1) - seqData.Mocks.ProfilesRepo = profilesRepoMock - - asyncSearchesRepoMock := mock_repo.NewMockAsyncSearches(ctrl) - asyncSearchesRepoMock.EXPECT().SaveAsyncSearch(gomock.Any(), gomock.Any()). - Return(tt.mockArgs.repoErr).Times(1) - seqData.Mocks.AsyncSearchesRepo = asyncSearchesRepoMock + seqData.Mocks.AsyncSearchesSvc = svcMock } - api := initTestAPIWithAsyncSearches(seqData) - req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/async_search/start", strings.NewReader(tt.reqBody)) - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, mockUserName)) + api := setupTestAPI(seqData) - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveStartAsyncSearch, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[startAsyncSearchRequest, startAsyncSearchResponse]{ + Method: http.MethodPost, + Target: "/seqapi/v1/async_search/start", + Req: tt.req, + Handler: api.serveStartAsyncSearch, + Want: tt.want, + WantErr: tt.wantErr, }) }) } @@ -151,13 +177,12 @@ func TestServeStartAsyncSearch(t *testing.T) { func TestServeStartAsyncSearch_Disabled(t *testing.T) { seqData := test.APITestData{} - api := initTestAPI(seqData) - req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/async_search/start", strings.NewReader("{}")) - - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveStartAsyncSearch, - WantRespBody: `{"message":"async searches disabled"}`, - WantStatus: http.StatusBadRequest, + api := setupTestAPI(seqData) + + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[startAsyncSearchRequest, struct{}]{ + Method: http.MethodPost, + Target: "/seqapi/v1/async_search/start", + Handler: api.serveStartAsyncSearch, + WantErr: true, }) } diff --git a/internal/api/seqapi/v1/http/test_data.go b/internal/api/seqapi/v1/http/test_data.go index 455b472..0f24d93 100644 --- a/internal/api/seqapi/v1/http/test_data.go +++ b/internal/api/seqapi/v1/http/test_data.go @@ -2,17 +2,27 @@ package http import ( "context" + "errors" + "net/http" + "time" + + "github.com/go-chi/chi/v5" - "github.com/ozontech/seq-ui/internal/api/profiles" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" "github.com/ozontech/seq-ui/internal/app/config" "github.com/ozontech/seq-ui/internal/pkg/client/seqdb" - "github.com/ozontech/seq-ui/internal/pkg/repository" - "github.com/ozontech/seq-ui/internal/pkg/service" asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches" ) -func initTestAPI(data test.APITestData) *API { +// Shared test data. +var ( + errSomethingWrong = errors.New("something happened wrong") + testSearchID = "69e4a4a6-0922-43bd-952d-060a86c2b622" + testQuery = "message:error" + testTimestamp = time.Date(2023, time.September, 25, 10, 20, 30, 0, time.UTC) +) + +func setupTestAPI(data test.APITestData) *API { // when test cases don't explicitly provide configuration. if data.Cfg.SeqAPIOptions == nil { data.Cfg.SeqAPIOptions = &config.SeqAPIOptions{} @@ -24,20 +34,19 @@ func initTestAPI(data test.APITestData) *API { seqDBClients[envConfig.SeqDB] = data.Mocks.SeqDB } - return New(data.Cfg, seqDBClients, data.Mocks.Cache, data.Mocks.Cache, nil, nil) + var asyncSvc asyncsearches.Service + if data.Mocks.AsyncSearchesSvc != nil { + asyncSvc = data.Mocks.AsyncSearchesSvc + } + + return New(data.Cfg, seqDBClients, data.Mocks.Cache, data.Mocks.Cache, asyncSvc) } -func initTestAPIWithAsyncSearches(data test.APITestData) *API { - if data.Cfg.SeqAPIOptions == nil { - data.Cfg.SeqAPIOptions = &config.SeqAPIOptions{} - } - seqDBClients := map[string]seqdb.Client{ - config.DefaultSeqDBClientID: data.Mocks.SeqDB, +func withQueryParamID(h http.HandlerFunc, id string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rCtx := chi.NewRouteContext() + rCtx.URLParams.Add("id", id) + r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rCtx)) + h(w, r) } - as := asyncsearches.New(context.Background(), data.Mocks.AsyncSearchesRepo, data.Mocks.SeqDB, data.AsyncCfg) - s := service.New(&repository.Repository{ - UserProfiles: data.Mocks.ProfilesRepo, - }) - p := profiles.New(s) - return New(data.Cfg, seqDBClients, data.Mocks.Cache, data.Mocks.Cache, as, p) } diff --git a/internal/api/seqapi/v1/seqapi.go b/internal/api/seqapi/v1/seqapi.go index 9cdae47..f1a08c4 100644 --- a/internal/api/seqapi/v1/seqapi.go +++ b/internal/api/seqapi/v1/seqapi.go @@ -3,7 +3,6 @@ package seqapi_v1 import ( "github.com/go-chi/chi/v5" - "github.com/ozontech/seq-ui/internal/api/profiles" grpc_api "github.com/ozontech/seq-ui/internal/api/seqapi/v1/grpc" http_api "github.com/ozontech/seq-ui/internal/api/seqapi/v1/http" "github.com/ozontech/seq-ui/internal/app/config" @@ -22,12 +21,11 @@ func New( seqDB map[string]seqdb.Client, inmemWithRedisCache cache.Cache, redisCache cache.Cache, - asyncSearches *asyncsearches.Service, - p *profiles.Profiles, + asyncSearches asyncsearches.Service, ) *SeqAPI { return &SeqAPI{ - grpcAPI: grpc_api.New(cfg, seqDB, inmemWithRedisCache, redisCache, asyncSearches, p), - httpAPI: http_api.New(cfg, seqDB, inmemWithRedisCache, redisCache, asyncSearches, p), + grpcAPI: grpc_api.New(cfg, seqDB, inmemWithRedisCache, redisCache, asyncSearches), + httpAPI: http_api.New(cfg, seqDB, inmemWithRedisCache, redisCache, asyncSearches), } } diff --git a/internal/api/seqapi/v1/test/data.go b/internal/api/seqapi/v1/test/data.go index 46ea63d..0ab1d44 100644 --- a/internal/api/seqapi/v1/test/data.go +++ b/internal/api/seqapi/v1/test/data.go @@ -9,7 +9,7 @@ import ( "github.com/ozontech/seq-ui/internal/app/config" mock_cache "github.com/ozontech/seq-ui/internal/pkg/cache/mock" mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" - mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) @@ -20,16 +20,14 @@ type CacheMockArgs struct { } type Mocks struct { - SeqDB *mock_seqdb.MockClient - Cache *mock_cache.MockCache - AsyncSearchesRepo *mock_repo.MockAsyncSearches - ProfilesRepo *mock_repo.MockUserProfiles + SeqDB *mock_seqdb.MockClient + Cache *mock_cache.MockCache + AsyncSearchesSvc *mock_asyncsearches.MockService } type APITestData struct { - Cfg config.SeqAPI - AsyncCfg config.AsyncSearch - Mocks Mocks + Cfg config.SeqAPI + Mocks Mocks } func MakeEvent(id string, countData int, t time.Time) *seqapi.Event { diff --git a/internal/api/userprofile/v1/grpc/api.go b/internal/api/userprofile/v1/grpc/api.go index 202863d..c3ff3c6 100644 --- a/internal/api/userprofile/v1/grpc/api.go +++ b/internal/api/userprofile/v1/grpc/api.go @@ -1,21 +1,18 @@ package grpc import ( - "github.com/ozontech/seq-ui/internal/api/profiles" - "github.com/ozontech/seq-ui/internal/pkg/service" - "github.com/ozontech/seq-ui/pkg/userprofile/v1" + "github.com/ozontech/seq-ui/internal/pkg/service/userprofile" + api "github.com/ozontech/seq-ui/pkg/userprofile/v1" ) type API struct { - userprofile.UnimplementedUserProfileServiceServer + api.UnimplementedUserProfileServiceServer - service service.Service - profiles *profiles.Profiles + service userprofile.Service } -func New(svc service.Service, p *profiles.Profiles) *API { +func New(svc userprofile.Service) *API { return &API{ - service: svc, - profiles: p, + service: svc, } } diff --git a/internal/api/userprofile/v1/grpc/favorite_queries.go b/internal/api/userprofile/v1/grpc/favorite_queries.go index 55a1f43..32b2631 100644 --- a/internal/api/userprofile/v1/grpc/favorite_queries.go +++ b/internal/api/userprofile/v1/grpc/favorite_queries.go @@ -16,14 +16,8 @@ func (a *API) GetFavoriteQueries(ctx context.Context, _ *userprofile.GetFavorite ctx, span := tracing.StartSpan(ctx, "userprofile_v1_get_favorite_queries") defer span.End() - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - return nil, grpcutil.ProcessError(err) - } + request := types.GetFavoriteQueriesRequest{} - request := types.GetFavoriteQueriesRequest{ - ProfileID: profileID, - } favoriteQueries, err := a.service.GetFavoriteQueries(ctx, request) if err != nil { return nil, grpcutil.ProcessError(err) @@ -54,21 +48,15 @@ func (a *API) CreateFavoriteQuery(ctx context.Context, req *userprofile.CreateFa }, ) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - return nil, grpcutil.ProcessError(err) - } + request := types.GetOrCreateFavoriteQueryRequest{Query: req.Query} - request := types.GetOrCreateFavoriteQueryRequest{ - ProfileID: profileID, - Query: req.Query, - } if req.RelativeFrom != nil { request.RelativeFrom = *req.RelativeFrom } if req.Name != nil { request.Name = *req.Name } + fqID, err := a.service.GetOrCreateFavoriteQuery(ctx, request) if err != nil { return nil, grpcutil.ProcessError(err) @@ -89,16 +77,9 @@ func (a *API) DeleteFavoriteQuery(ctx context.Context, req *userprofile.DeleteFa Value: attribute.Int64Value(req.GetId()), }) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - return nil, grpcutil.ProcessError(err) - } + request := types.DeleteFavoriteQueryRequest{ID: req.Id} - request := types.DeleteFavoriteQueryRequest{ - ID: req.Id, - ProfileID: profileID, - } - if err = a.service.DeleteFavoriteQuery(ctx, request); err != nil { + if err := a.service.DeleteFavoriteQuery(ctx, request); err != nil { return nil, grpcutil.ProcessError(err) } diff --git a/internal/api/userprofile/v1/grpc/favorite_queries_test.go b/internal/api/userprofile/v1/grpc/favorite_queries_test.go index b2e4f52..805ba84 100644 --- a/internal/api/userprofile/v1/grpc/favorite_queries_test.go +++ b/internal/api/userprofile/v1/grpc/favorite_queries_test.go @@ -2,7 +2,6 @@ package grpc import ( "context" - "errors" "testing" "github.com/stretchr/testify/require" @@ -15,10 +14,10 @@ import ( ) func TestGetFavoriteQueries(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - queryName := "my query" - var relativeFrom uint64 = 300 + var ( + relativeFrom uint64 = 300 + queryName = "my query" + ) type mockArgs struct { req types.GetFavoriteQueriesRequest @@ -33,10 +32,9 @@ func TestGetFavoriteQueries(t *testing.T) { wantCode codes.Code mockArgs *mockArgs - noUser bool }{ { - name: "success", + name: "ok", want: &userprofile.GetFavoriteQueriesResponse{ Queries: []*userprofile.GetFavoriteQueriesResponse_Query{ { @@ -65,9 +63,6 @@ func TestGetFavoriteQueries(t *testing.T) { }, wantCode: codes.OK, mockArgs: &mockArgs{ - req: types.GetFavoriteQueriesRequest{ - ProfileID: profileID, - }, resp: types.FavoriteQueries{ { ID: 1, @@ -95,40 +90,28 @@ func TestGetFavoriteQueries(t *testing.T) { }, }, { - name: "err_no_user", - wantCode: codes.Unauthenticated, - noUser: true, - }, - { - name: "err_repo_random", + name: "err_svc", wantCode: codes.Internal, mockArgs: &mockArgs{ - req: types.GetFavoriteQueriesRequest{ - ProfileID: profileID, - }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newFavoriteQueriesTestData(t) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().GetAll(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) + mockedSvc.EXPECT(). + GetFavoriteQueries(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - ctx := context.Background() - if !tt.noUser { - ctx = context.WithValue(ctx, types.UserKey{}, userName) - api.profiles.SetID(userName, profileID) - } - - got, err := api.GetFavoriteQueries(ctx, &userprofile.GetFavoriteQueriesRequest{}) + got, err := api.GetFavoriteQueries(context.Background(), &userprofile.GetFavoriteQueriesRequest{}) require.Equal(t, tt.wantCode, status.Code(err)) if tt.wantCode != codes.OK { @@ -141,12 +124,12 @@ func TestGetFavoriteQueries(t *testing.T) { } func TestCreateFavoriteQuery(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - var queryID int64 = 1 - query := "test" - queryName := "my query" - var relativeFrom uint64 = 300 + var ( + queryID int64 = 1 + relativeFrom uint64 = 300 + query = "test" + queryName = "my query" + ) type mockArgs struct { req types.GetOrCreateFavoriteQueryRequest @@ -162,10 +145,9 @@ func TestCreateFavoriteQuery(t *testing.T) { wantCode codes.Code mockArgs *mockArgs - noUser bool }{ { - name: "success", + name: "ok", req: &userprofile.CreateFavoriteQueryRequest{ Query: query, Name: &queryName, @@ -177,7 +159,6 @@ func TestCreateFavoriteQuery(t *testing.T) { wantCode: codes.OK, mockArgs: &mockArgs{ req: types.GetOrCreateFavoriteQueryRequest{ - ProfileID: profileID, Query: query, Name: queryName, RelativeFrom: relativeFrom, @@ -186,66 +167,32 @@ func TestCreateFavoriteQuery(t *testing.T) { }, }, { - name: "success_only_query", - req: &userprofile.CreateFavoriteQueryRequest{ - Query: query, - }, - want: &userprofile.CreateFavoriteQueryResponse{ - Id: queryID, - }, - wantCode: codes.OK, - mockArgs: &mockArgs{ - req: types.GetOrCreateFavoriteQueryRequest{ - ProfileID: profileID, - Query: query, - }, - resp: queryID, - }, - }, - { - name: "err_no_user", - wantCode: codes.Unauthenticated, - noUser: true, - }, - { - name: "err_svc_empty_query", - req: &userprofile.CreateFavoriteQueryRequest{}, - wantCode: codes.InvalidArgument, - }, - { - name: "err_repo_random", + name: "err_svc", req: &userprofile.CreateFavoriteQueryRequest{ Query: query, }, wantCode: codes.Internal, mockArgs: &mockArgs{ - req: types.GetOrCreateFavoriteQueryRequest{ - ProfileID: profileID, - Query: query, - }, - err: errors.New("random repo err"), + req: types.GetOrCreateFavoriteQueryRequest{Query: query}, + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newFavoriteQueriesTestData(t) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().GetOrCreate(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - - ctx := context.Background() - if !tt.noUser { - ctx = context.WithValue(ctx, types.UserKey{}, userName) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + GetOrCreateFavoriteQuery(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - got, err := api.CreateFavoriteQuery(ctx, tt.req) + got, err := api.CreateFavoriteQuery(context.Background(), tt.req) require.Equal(t, tt.wantCode, status.Code(err)) if tt.wantCode != codes.OK { @@ -258,9 +205,9 @@ func TestCreateFavoriteQuery(t *testing.T) { } func TestDeleteFavoriteQuery(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - var queryID int64 = 100 + var ( + queryID int64 = 1 + ) type mockArgs struct { req types.DeleteFavoriteQueryRequest @@ -275,10 +222,9 @@ func TestDeleteFavoriteQuery(t *testing.T) { wantCode codes.Code mockArgs *mockArgs - noUser bool }{ { - name: "success", + name: "ok", req: &userprofile.DeleteFavoriteQueryRequest{ Id: queryID, }, @@ -286,57 +232,39 @@ func TestDeleteFavoriteQuery(t *testing.T) { wantCode: codes.OK, mockArgs: &mockArgs{ req: types.DeleteFavoriteQueryRequest{ - ID: queryID, - ProfileID: profileID, + ID: queryID, }, }, }, { - name: "err_no_user", - wantCode: codes.Unauthenticated, - noUser: true, - }, - { - name: "err_svc_invalid_id", - req: &userprofile.DeleteFavoriteQueryRequest{ - Id: -100, - }, - wantCode: codes.InvalidArgument, - }, - { - name: "err_repo_random", + name: "err_svc", req: &userprofile.DeleteFavoriteQueryRequest{ Id: queryID, }, wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.DeleteFavoriteQueryRequest{ - ID: queryID, - ProfileID: profileID, + ID: queryID, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newFavoriteQueriesTestData(t) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().Delete(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.err).Times(1) - } - - ctx := context.Background() - if !tt.noUser { - ctx = context.WithValue(ctx, types.UserKey{}, userName) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + DeleteFavoriteQuery(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.err). + Times(1) } - got, err := api.DeleteFavoriteQuery(ctx, tt.req) + got, err := api.DeleteFavoriteQuery(context.Background(), tt.req) require.Equal(t, tt.wantCode, status.Code(err)) if tt.wantCode != codes.OK { diff --git a/internal/api/userprofile/v1/grpc/test_data.go b/internal/api/userprofile/v1/grpc/test_data.go index 2860f1a..8d79006 100644 --- a/internal/api/userprofile/v1/grpc/test_data.go +++ b/internal/api/userprofile/v1/grpc/test_data.go @@ -1,18 +1,27 @@ package grpc import ( + "context" + "errors" "testing" - "github.com/ozontech/seq-ui/internal/api/userprofile/v1/test" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + "go.uber.org/mock/gomock" + + "github.com/ozontech/seq-ui/internal/app/types" + mock "github.com/ozontech/seq-ui/internal/pkg/service/userprofile/mock" +) + +// Shared test data. +var ( + errSomethingWrong = errors.New("something happened wrong") ) -func newUserProfilesTestData(t *testing.T) (*API, *repo_mock.MockUserProfiles) { - mock, s, p := test.NewUserProfilesData(t) - return New(s, p), mock +func setupTestAPI(t *testing.T) (*API, *mock.MockService) { + ctrl := gomock.NewController(t) + mockedSvc := mock.NewMockService(ctrl) + return New(mockedSvc), mockedSvc } -func newFavoriteQueriesTestData(t *testing.T) (*API, *repo_mock.MockFavoriteQueries) { - mock, s, p := test.NewFavoriteQueriesTestData(t) - return New(s, p), mock +func withUser(userName string) context.Context { + return context.WithValue(context.Background(), types.UserKey{}, userName) } diff --git a/internal/api/userprofile/v1/grpc/user_profiles.go b/internal/api/userprofile/v1/grpc/user_profiles.go index 45371d2..550fd68 100644 --- a/internal/api/userprofile/v1/grpc/user_profiles.go +++ b/internal/api/userprofile/v1/grpc/user_profiles.go @@ -21,16 +21,13 @@ func (a *API) GetUserProfile(ctx context.Context, _ *userprofile.GetUserProfileR return nil, grpcutil.ProcessError(err) } - request := types.GetOrCreateUserProfileRequest{ - UserName: userName, - } + request := types.GetOrCreateUserProfileRequest{UserName: userName} + userProfile, err := a.service.GetOrCreateUserProfile(ctx, request) if err != nil { return nil, grpcutil.ProcessError(err) } - a.profiles.SetID(userName, userProfile.ID) - return userProfile.ToProto(), nil } @@ -60,6 +57,7 @@ func (a *API) UpdateUserProfile(ctx context.Context, req *userprofile.UpdateUser Timezone: req.Timezone, OnboardingVersion: req.OnboardingVersion, } + if req.GetLogColumns() != nil { request.LogColumns = &types.LogColumns{LogColumns: req.GetLogColumns().GetLogColumns()} } diff --git a/internal/api/userprofile/v1/grpc/user_profiles_test.go b/internal/api/userprofile/v1/grpc/user_profiles_test.go index 444db8b..4913bb6 100644 --- a/internal/api/userprofile/v1/grpc/user_profiles_test.go +++ b/internal/api/userprofile/v1/grpc/user_profiles_test.go @@ -1,8 +1,6 @@ package grpc import ( - "context" - "errors" "testing" "github.com/stretchr/testify/require" @@ -15,10 +13,12 @@ import ( ) func TestGetUserProfile(t *testing.T) { - userName := "unnamed" - timezone := "UTC" - onboardingVersion := `{"name1": "ver1", "name2": "ver2"}` - logColumns := []string{"val1", "val2"} + var ( + userName = "unnamed" + timezone = "UTC" + onboardingVersion = `{"name1": "ver1", "name2": "ver2"}` + logColumns = []string{"val1", "val2"} + ) type mockArgs struct { req types.GetOrCreateUserProfileRequest @@ -33,10 +33,9 @@ func TestGetUserProfile(t *testing.T) { wantCode codes.Code mockArgs *mockArgs - noUser bool }{ { - name: "success", + name: "ok", want: &userprofile.GetUserProfileResponse{ Timezone: timezone, OnboardingVersion: onboardingVersion, @@ -57,38 +56,31 @@ func TestGetUserProfile(t *testing.T) { }, }, { - name: "err_no_user", - wantCode: codes.Unauthenticated, - noUser: true, - }, - { - name: "err_repo_random", + name: "err_svc", wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.GetOrCreateUserProfileRequest{ UserName: userName, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newUserProfilesTestData(t) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().GetOrCreate(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - - ctx := context.Background() - if !tt.noUser { - ctx = context.WithValue(ctx, types.UserKey{}, userName) + mockedSvc.EXPECT(). + GetOrCreateUserProfile(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } + ctx := withUser(userName) got, err := api.GetUserProfile(ctx, &userprofile.GetUserProfileRequest{}) require.Equal(t, tt.wantCode, status.Code(err)) @@ -102,11 +94,12 @@ func TestGetUserProfile(t *testing.T) { } func TestUpdateUserProfile(t *testing.T) { - userName := "unnamed" - validTimezone := "Europe/Moscow" - invalidTimezone := "invalid timezone" - onboardingVersion := `{"name1": "ver1", "name2": "ver2"}` - logColumns := []string{"val1", "val2"} + var ( + userName = "unnamed" + validTimezone = "Europe/Moscow" + onboardingVersion = `{"name1": "ver1", "name2": "ver2"}` + logColumns = []string{"val1", "val2"} + ) type mockArgs struct { req types.UpdateUserProfileRequest @@ -121,10 +114,9 @@ func TestUpdateUserProfile(t *testing.T) { wantCode codes.Code mockArgs *mockArgs - noUser bool }{ { - name: "success_all", + name: "ok", req: &userprofile.UpdateUserProfileRequest{ Timezone: &validTimezone, OnboardingVersion: &onboardingVersion, @@ -142,94 +134,7 @@ func TestUpdateUserProfile(t *testing.T) { }, }, { - name: "success_only_timezone", - req: &userprofile.UpdateUserProfileRequest{ - Timezone: &validTimezone, - }, - want: &userprofile.UpdateUserProfileResponse{}, - wantCode: codes.OK, - mockArgs: &mockArgs{ - req: types.UpdateUserProfileRequest{ - UserName: userName, - Timezone: &validTimezone, - }, - }, - }, - { - name: "success_only_onboarding_ver", - req: &userprofile.UpdateUserProfileRequest{ - OnboardingVersion: &onboardingVersion, - }, - want: &userprofile.UpdateUserProfileResponse{}, - wantCode: codes.OK, - mockArgs: &mockArgs{ - req: types.UpdateUserProfileRequest{ - UserName: userName, - OnboardingVersion: &onboardingVersion, - }, - }, - }, - { - name: "success_only_log_columns", - req: &userprofile.UpdateUserProfileRequest{ - LogColumns: &userprofile.LogColumns{LogColumns: logColumns}, - }, - want: &userprofile.UpdateUserProfileResponse{}, - wantCode: codes.OK, - mockArgs: &mockArgs{ - req: types.UpdateUserProfileRequest{ - UserName: userName, - LogColumns: &types.LogColumns{LogColumns: logColumns}, - }, - }, - }, - { - name: "success_empty_log_columns", - req: &userprofile.UpdateUserProfileRequest{ - LogColumns: &userprofile.LogColumns{LogColumns: []string{}}, - }, - want: &userprofile.UpdateUserProfileResponse{}, - wantCode: codes.OK, - mockArgs: &mockArgs{ - req: types.UpdateUserProfileRequest{ - UserName: userName, - LogColumns: &types.LogColumns{LogColumns: []string{}}, - }, - }, - }, - { - name: "err_no_user", - wantCode: codes.Unauthenticated, - noUser: true, - }, - { - name: "err_svc_empty_request", - req: &userprofile.UpdateUserProfileRequest{}, - wantCode: codes.InvalidArgument, - }, - { - name: "err_svc_invalid_timezone_format", - req: &userprofile.UpdateUserProfileRequest{ - Timezone: &invalidTimezone, - }, - wantCode: codes.InvalidArgument, - }, - { - name: "err_repo_not_found", - req: &userprofile.UpdateUserProfileRequest{ - Timezone: &validTimezone, - }, - wantCode: codes.NotFound, - mockArgs: &mockArgs{ - req: types.UpdateUserProfileRequest{ - UserName: userName, - Timezone: &validTimezone, - }, - err: types.ErrNotFound, - }, - }, - { - name: "err_repo_random", + name: "err_svc", req: &userprofile.UpdateUserProfileRequest{ Timezone: &validTimezone, }, @@ -239,27 +144,25 @@ func TestUpdateUserProfile(t *testing.T) { UserName: userName, Timezone: &validTimezone, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newUserProfilesTestData(t) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().Update(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.err).Times(1) - } - - ctx := context.Background() - if !tt.noUser { - ctx = context.WithValue(ctx, types.UserKey{}, userName) + mockedSvc.EXPECT(). + UpdateUserProfile(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.err). + Times(1) } + ctx := withUser(userName) got, err := api.UpdateUserProfile(ctx, tt.req) require.Equal(t, tt.wantCode, status.Code(err)) diff --git a/internal/api/userprofile/v1/http/api.go b/internal/api/userprofile/v1/http/api.go index b817660..59377eb 100644 --- a/internal/api/userprofile/v1/http/api.go +++ b/internal/api/userprofile/v1/http/api.go @@ -3,19 +3,16 @@ package http import ( "github.com/go-chi/chi/v5" - "github.com/ozontech/seq-ui/internal/api/profiles" - "github.com/ozontech/seq-ui/internal/pkg/service" + "github.com/ozontech/seq-ui/internal/pkg/service/userprofile" ) type API struct { - service service.Service - profiles *profiles.Profiles + service userprofile.Service } -func New(svc service.Service, p *profiles.Profiles) *API { +func New(svc userprofile.Service) *API { return &API{ - service: svc, - profiles: p, + service: svc, } } diff --git a/internal/api/userprofile/v1/http/favorite_queries.go b/internal/api/userprofile/v1/http/favorite_queries.go index 8a72eb7..ba638dd 100644 --- a/internal/api/userprofile/v1/http/favorite_queries.go +++ b/internal/api/userprofile/v1/http/favorite_queries.go @@ -29,16 +29,9 @@ func (a *API) serveGetFavoriteQueries(w http.ResponseWriter, r *http.Request) { wr := httputil.NewWriter(w) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - httputil.ProcessError(wr, err) - return - } - - req := types.GetFavoriteQueriesRequest{ - ProfileID: profileID, - } + req := types.GetFavoriteQueriesRequest{} fqs, err := a.service.GetFavoriteQueries(ctx, req) + if err != nil { httputil.ProcessError(wr, err) return @@ -85,26 +78,20 @@ func (a *API) serveCreateFavoriteQuery(w http.ResponseWriter, r *http.Request) { }, ) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - httputil.ProcessError(wr, err) - return - } + req := types.GetOrCreateFavoriteQueryRequest{Query: httpReq.Query} - req := types.GetOrCreateFavoriteQueryRequest{ - ProfileID: profileID, - Query: httpReq.Query, - } if httpReq.Name != nil { req.Name = *httpReq.Name } if httpReq.RelativeFrom != nil { + var err error req.RelativeFrom, err = strconv.ParseUint(*httpReq.RelativeFrom, 10, 64) if err != nil { wr.Error(errors.New("incorrect favorite query 'relativeFrom' format"), http.StatusBadRequest) return } } + fqID, err := a.service.GetOrCreateFavoriteQuery(ctx, req) if err != nil { httputil.ProcessError(wr, err) @@ -142,16 +129,8 @@ func (a *API) serveDeleteFavoriteQuery(w http.ResponseWriter, r *http.Request) { Value: attribute.Int64Value(id), }) - profileID, err := a.profiles.GeIDFromContext(ctx) - if err != nil { - httputil.ProcessError(wr, err) - return - } + req := types.DeleteFavoriteQueryRequest{ID: id} - req := types.DeleteFavoriteQueryRequest{ - ID: id, - ProfileID: profileID, - } err = a.service.DeleteFavoriteQuery(ctx, req) if err != nil { httputil.ProcessError(wr, err) diff --git a/internal/api/userprofile/v1/http/favorite_queries_test.go b/internal/api/userprofile/v1/http/favorite_queries_test.go index a280567..ccb6189 100644 --- a/internal/api/userprofile/v1/http/favorite_queries_test.go +++ b/internal/api/userprofile/v1/http/favorite_queries_test.go @@ -1,15 +1,11 @@ package http import ( - "context" - "errors" "fmt" "net/http" - "net/http/httptest" - "strings" + "strconv" "testing" - "github.com/go-chi/chi/v5" "go.uber.org/mock/gomock" "github.com/ozontech/seq-ui/internal/api/httputil" @@ -17,8 +13,9 @@ import ( ) func TestServeGetFavoriteQueries(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 + var ( + relativeFrom = "300" + ) type mockArgs struct { req types.GetFavoriteQueriesRequest @@ -29,105 +26,69 @@ func TestServeGetFavoriteQueries(t *testing.T) { tests := []struct { name string - wantRespBody string - wantStatus int + want getFavoriteQueriesResponse + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success", - wantRespBody: `{"queries":[{"id":"1","query":"test1","name":"my query 1","relativeFrom":"300"},{"id":"2","query":"test2","name":"my query 2"},{"id":"3","query":"test3","relativeFrom":"900"},{"id":"4","query":"test4"}]}`, - wantStatus: http.StatusOK, - mockArgs: &mockArgs{ - req: types.GetFavoriteQueriesRequest{ - ProfileID: profileID, + name: "ok", + want: getFavoriteQueriesResponse{ + Queries: favoriteQueries{ + {ID: "1", Query: "test1", Name: "my query 1", RelativeFrom: relativeFrom}, + {ID: "2", Query: "test2", Name: "my query 2"}, + {ID: "3", Query: "test3", RelativeFrom: relativeFrom}, + {ID: "4", Query: "test4"}, }, + }, + mockArgs: &mockArgs{ resp: types.FavoriteQueries{ - { - ID: 1, - Query: "test1", - Name: "my query 1", - RelativeFrom: 300, - }, - { - ID: 2, - Query: "test2", - Name: "my query 2", - }, - { - ID: 3, - Query: "test3", - RelativeFrom: 900, - }, - { - ID: 4, - Query: "test4", - }, + {ID: 1, Query: "test1", Name: "my query 1", RelativeFrom: 300}, + {ID: 2, Query: "test2", Name: "my query 2"}, + {ID: 3, Query: "test3", RelativeFrom: 300}, + {ID: 4, Query: "test4"}, }, }, }, { - name: "err_no_user", - wantStatus: http.StatusUnauthorized, - noUser: true, - }, - { - name: "err_repo_random", - wantStatus: http.StatusInternalServerError, + name: "err_svc", + wantErr: true, mockArgs: &mockArgs{ - req: types.GetFavoriteQueriesRequest{ - ProfileID: profileID, - }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newFavoriteQueriesTestData(t) - req := httptest.NewRequest(http.MethodGet, "/userprofile/v1/queries/favorite", http.NoBody) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().GetAll(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - if !tt.noUser { - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, userName)) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + GetFavoriteQueries(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetFavoriteQueries, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getFavoriteQueriesResponse]{ + Method: http.MethodGet, + Target: "/userprofile/v1/queries/favorite", + Handler: api.serveGetFavoriteQueries, + Want: tt.want, + WantErr: tt.wantErr, }) }) } } func TestServeCreateFavoriteQuery(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - var queryID int64 = 1 - query := "test" - - formatReqBody := func(query, name, relativeFrom string) string { - var sb strings.Builder - sb.WriteString(fmt.Sprintf(`{"query":%q`, query)) - if name != "" { - sb.WriteString(fmt.Sprintf(`,"name":%q`, name)) - } - if relativeFrom != "" { - sb.WriteString(fmt.Sprintf(`,"relativeFrom":%q`, relativeFrom)) - } - sb.WriteString("}") - return sb.String() - } + var ( + relativeFrom = "300" + query = "test" + queryName = "my query" + ) type mockArgs struct { req types.GetOrCreateFavoriteQueryRequest @@ -138,107 +99,66 @@ func TestServeCreateFavoriteQuery(t *testing.T) { tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req createFavoriteQueryRequest + want createFavoriteQueryResponse + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success", - reqBody: formatReqBody(query, "my query", "300"), - wantRespBody: fmt.Sprintf(`{"id":"%d"}`, queryID), - wantStatus: http.StatusOK, + name: "ok", + req: createFavoriteQueryRequest{Query: query, Name: &queryName, RelativeFrom: &relativeFrom}, + want: createFavoriteQueryResponse{ID: "1"}, mockArgs: &mockArgs{ req: types.GetOrCreateFavoriteQueryRequest{ - ProfileID: profileID, Query: query, Name: "my query", RelativeFrom: 300, }, - resp: queryID, + resp: 1, }, }, { - name: "success_only_query", - reqBody: formatReqBody(query, "", ""), - wantRespBody: fmt.Sprintf(`{"id":"%d"}`, queryID), - wantStatus: http.StatusOK, + name: "err_svc", + req: createFavoriteQueryRequest{Query: query, Name: &queryName, RelativeFrom: &relativeFrom}, + wantErr: true, mockArgs: &mockArgs{ req: types.GetOrCreateFavoriteQueryRequest{ - ProfileID: profileID, - Query: query, - }, - resp: queryID, - }, - }, - { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, - noUser: true, - }, - { - name: "err_no_user", - reqBody: formatReqBody(query, "", ""), - wantStatus: http.StatusUnauthorized, - noUser: true, - }, - { - name: "err_invalid_relative_from_format", - reqBody: formatReqBody(query, "", "not_number"), - wantStatus: http.StatusBadRequest, - }, - { - name: "err_svc_empty_query", - reqBody: formatReqBody("", "", ""), - wantStatus: http.StatusBadRequest, - }, - { - name: "err_repo_random", - reqBody: formatReqBody(query, "", ""), - wantStatus: http.StatusInternalServerError, - mockArgs: &mockArgs{ - req: types.GetOrCreateFavoriteQueryRequest{ - ProfileID: profileID, - Query: query, + Query: query, + Name: "my query", + RelativeFrom: 300, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newFavoriteQueriesTestData(t) - req := httptest.NewRequest(http.MethodPost, "/userprofile/v1/queries/favorite", strings.NewReader(tt.reqBody)) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().GetOrCreate(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - if !tt.noUser { - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, userName)) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + GetOrCreateFavoriteQuery(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveCreateFavoriteQuery, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[createFavoriteQueryRequest, createFavoriteQueryResponse]{ + Method: http.MethodPost, + Target: "/userprofile/v1/queries/favorite", + Req: tt.req, + Handler: api.serveCreateFavoriteQuery, + Want: tt.want, + WantErr: tt.wantErr, }) }) } } func TestServeDeleteFavoriteQuery(t *testing.T) { - userName := "unnamed" - var profileID int64 = 1 - type mockArgs struct { req types.DeleteFavoriteQueryRequest err error @@ -247,77 +167,52 @@ func TestServeDeleteFavoriteQuery(t *testing.T) { tests := []struct { name string - id string - wantStatus int + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success", - id: "100", - wantStatus: http.StatusOK, + name: "ok", mockArgs: &mockArgs{ req: types.DeleteFavoriteQueryRequest{ - ID: 100, - ProfileID: profileID, + ID: 100, }, }, }, { - name: "err_invalid_id_format", - id: "not_number", - wantStatus: http.StatusBadRequest, - noUser: true, - }, - { - name: "err_no_user", - id: "100", - wantStatus: http.StatusUnauthorized, - noUser: true, - }, - { - name: "err_svc_invalid_id", - id: "-100", - wantStatus: http.StatusBadRequest, - }, - { - name: "err_repo_random", - id: "100", - wantStatus: http.StatusInternalServerError, + name: "err_svc", + wantErr: true, mockArgs: &mockArgs{ req: types.DeleteFavoriteQueryRequest{ - ID: 100, - ProfileID: profileID, + ID: 100, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newFavoriteQueriesTestData(t) - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/userprofile/v1/queries/favorite/%s", tt.id), http.NoBody) - rCtx := chi.NewRouteContext() - rCtx.URLParams.Add("id", tt.id) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rCtx)) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().Delete(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.err).Times(1) - } - if !tt.noUser { - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, userName)) - api.profiles.SetID(userName, profileID) + mockedSvc.EXPECT(). + DeleteFavoriteQuery(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.err). + Times(1) } - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveDeleteFavoriteQuery, - WantStatus: tt.wantStatus, + id := strconv.FormatInt(tt.mockArgs.req.ID, 10) + handler := withID(api.serveDeleteFavoriteQuery, id) + + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, struct{}]{ + Method: http.MethodDelete, + Target: fmt.Sprintf("/userprofile/v1/queries/favorite/%s", id), + Handler: handler, + NoResp: true, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/userprofile/v1/http/test_data.go b/internal/api/userprofile/v1/http/test_data.go index fcfb67a..71b8e99 100644 --- a/internal/api/userprofile/v1/http/test_data.go +++ b/internal/api/userprofile/v1/http/test_data.go @@ -1,18 +1,41 @@ package http import ( + "context" + "errors" + "net/http" "testing" - "github.com/ozontech/seq-ui/internal/api/userprofile/v1/test" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository/mock" + "github.com/go-chi/chi/v5" + "go.uber.org/mock/gomock" + + "github.com/ozontech/seq-ui/internal/app/types" + mock "github.com/ozontech/seq-ui/internal/pkg/service/userprofile/mock" +) + +// Shared test data. +var ( + errSomethingWrong = errors.New("something happened wrong") ) -func newUserProfilesTestData(t *testing.T) (*API, *repo_mock.MockUserProfiles) { - mock, s, p := test.NewUserProfilesData(t) - return New(s, p), mock +func setupTestAPI(t *testing.T) (*API, *mock.MockService) { + ctrl := gomock.NewController(t) + mockedSvc := mock.NewMockService(ctrl) + return New(mockedSvc), mockedSvc +} + +func withUser(h http.HandlerFunc, userName string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + r = r.WithContext(context.WithValue(r.Context(), types.UserKey{}, userName)) + h(w, r) + } } -func newFavoriteQueriesTestData(t *testing.T) (*API, *repo_mock.MockFavoriteQueries) { - mock, s, p := test.NewFavoriteQueriesTestData(t) - return New(s, p), mock +func withID(h http.HandlerFunc, id string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rCtx := chi.NewRouteContext() + rCtx.URLParams.Add("id", id) + r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rCtx)) + h(w, r) + } } diff --git a/internal/api/userprofile/v1/http/user_profiles.go b/internal/api/userprofile/v1/http/user_profiles.go index e008f0b..e116c67 100644 --- a/internal/api/userprofile/v1/http/user_profiles.go +++ b/internal/api/userprofile/v1/http/user_profiles.go @@ -35,14 +35,13 @@ func (a *API) serveGetUserProfile(w http.ResponseWriter, r *http.Request) { req := types.GetOrCreateUserProfileRequest{ UserName: userName, } + up, err := a.service.GetOrCreateUserProfile(ctx, req) if err != nil { httputil.ProcessError(wr, err) return } - a.profiles.SetID(userName, up.ID) - wr.WriteJson(newUserProfile(up)) } @@ -93,6 +92,7 @@ func (a *API) serveUpdateUserProfile(w http.ResponseWriter, r *http.Request) { Timezone: httpReq.Timezone, OnboardingVersion: httpReq.OnboardingVersion, } + if httpReq.LogColumns != nil { req.LogColumns = &types.LogColumns{LogColumns: httpReq.LogColumns.Columns} } diff --git a/internal/api/userprofile/v1/http/user_profiles_test.go b/internal/api/userprofile/v1/http/user_profiles_test.go index 2cdfd30..2f41d09 100644 --- a/internal/api/userprofile/v1/http/user_profiles_test.go +++ b/internal/api/userprofile/v1/http/user_profiles_test.go @@ -1,13 +1,7 @@ package http import ( - "context" - "encoding/json" - "errors" - "fmt" "net/http" - "net/http/httptest" - "strings" "testing" "go.uber.org/mock/gomock" @@ -17,10 +11,12 @@ import ( ) func TestServeGetUserProfile(t *testing.T) { - userName := "unnamed" - timezone := "UTC" - onboardingVersion := `{"name1": "ver1", "name2": "ver2"}` - logColumns := []string{"val1", "val2"} + var ( + userName = "unnamed" + timezone = "UTC" + onboardingVersion = `{"name1": "ver1", "name2": "ver2"}` + logColumns = []string{"val1", "val2"} + ) type mockArgs struct { req types.GetOrCreateUserProfileRequest @@ -31,16 +27,18 @@ func TestServeGetUserProfile(t *testing.T) { tests := []struct { name string - wantRespBody string - wantStatus int + want userProfile + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success", - wantRespBody: `{"timezone":"UTC","onboardingVersion":"{\"name1\": \"ver1\", \"name2\": \"ver2\"}","log_columns":["val1","val2"]}`, - wantStatus: http.StatusOK, + name: "ok", + want: userProfile{ + Timezone: timezone, + OnboardingVersion: onboardingVersion, + LogColumns: logColumns, + }, mockArgs: &mockArgs{ req: types.GetOrCreateUserProfileRequest{ UserName: userName, @@ -55,76 +53,48 @@ func TestServeGetUserProfile(t *testing.T) { }, }, { - name: "err_no_user", - wantStatus: http.StatusUnauthorized, - noUser: true, - }, - { - name: "err_repo_random", - wantStatus: http.StatusInternalServerError, + name: "err_svc", + wantErr: true, mockArgs: &mockArgs{ req: types.GetOrCreateUserProfileRequest{ UserName: userName, }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newUserProfilesTestData(t) - req := httptest.NewRequest(http.MethodGet, "/userprofile/v1/profile", http.NoBody) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().GetOrCreate(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) - } - if !tt.noUser { - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, userName)) + mockedSvc.EXPECT(). + GetOrCreateUserProfile(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err). + Times(1) } - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetUserProfile, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, userProfile]{ + Method: http.MethodGet, + Target: "/userprofile/v1/profile", + Handler: withUser(api.serveGetUserProfile, userName), + Want: tt.want, + WantErr: tt.wantErr, }) }) } } func TestServeUpdateUserProfile(t *testing.T) { - userName := "unnamed" - validTimezone := "Europe/Moscow" - invalidTimezone := "invalid timezone" - onboardingVersion := `{"name1": "ver1", "name2": "ver2"}` - logColumns := []string{"val1", "val2"} - - formatReqBody := func(timezone, onboardingVersion string, logColumns []string) string { - var sb strings.Builder - sb.WriteString("{") - if timezone != "" { - sb.WriteString(fmt.Sprintf(`"timezone":%q`, timezone)) - } - if onboardingVersion != "" { - if sb.Len() > 1 { - sb.WriteString(",") - } - sb.WriteString(fmt.Sprintf(`"onboardingVersion":%q`, onboardingVersion)) - } - if logColumns != nil { - if sb.Len() > 1 { - sb.WriteString(",") - } - v, _ := json.Marshal(logColumns) - sb.WriteString(fmt.Sprintf(`"log_columns":{"columns":%s}`, v)) - } - sb.WriteString("}") - return sb.String() - } + var ( + userName = "unnamed" + validTimezone = "Europe/Moscow" + onboardingVersion = `{"name1": "ver1", "name2": "ver2"}` + logColumns = []string{"val1", "val2"} + ) type mockArgs struct { req types.UpdateUserProfileRequest @@ -134,16 +104,20 @@ func TestServeUpdateUserProfile(t *testing.T) { tests := []struct { name string - reqBody string - wantStatus int + req updateUserProfileRequest + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success_all", - reqBody: formatReqBody(validTimezone, onboardingVersion, logColumns), - wantStatus: http.StatusOK, + name: "ok", + req: updateUserProfileRequest{ + Timezone: &validTimezone, + OnboardingVersion: &onboardingVersion, + LogColumns: &struct { + Columns []string "json:\"columns\"" + }{Columns: logColumns}, + }, mockArgs: &mockArgs{ req: types.UpdateUserProfileRequest{ UserName: userName, @@ -154,116 +128,47 @@ func TestServeUpdateUserProfile(t *testing.T) { }, }, { - name: "success_only_timezone", - reqBody: formatReqBody(validTimezone, "", nil), - wantStatus: http.StatusOK, - mockArgs: &mockArgs{ - req: types.UpdateUserProfileRequest{ - UserName: userName, - Timezone: &validTimezone, - }, + name: "err_svc", + req: updateUserProfileRequest{ + Timezone: &validTimezone, + OnboardingVersion: &onboardingVersion, + LogColumns: &struct { + Columns []string "json:\"columns\"" + }{Columns: logColumns}, }, - }, - { - name: "success_only_onboarding_ver", - reqBody: formatReqBody("", onboardingVersion, nil), - wantStatus: http.StatusOK, + wantErr: true, mockArgs: &mockArgs{ req: types.UpdateUserProfileRequest{ UserName: userName, + Timezone: &validTimezone, OnboardingVersion: &onboardingVersion, + LogColumns: &types.LogColumns{LogColumns: logColumns}, }, - }, - }, - { - name: "success_only_log_columns", - reqBody: formatReqBody("", "", logColumns), - wantStatus: http.StatusOK, - mockArgs: &mockArgs{ - req: types.UpdateUserProfileRequest{ - UserName: userName, - LogColumns: &types.LogColumns{LogColumns: logColumns}, - }, - }, - }, - { - name: "success_empty_log_columns", - reqBody: formatReqBody("", "", []string{}), - wantStatus: http.StatusOK, - mockArgs: &mockArgs{ - req: types.UpdateUserProfileRequest{ - UserName: userName, - LogColumns: &types.LogColumns{LogColumns: []string{}}, - }, - }, - }, - { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, - noUser: true, - }, - { - name: "err_no_user", - reqBody: formatReqBody(validTimezone, "", nil), - wantStatus: http.StatusUnauthorized, - noUser: true, - }, - { - name: "err_svc_empty_request", - reqBody: `{}`, - wantStatus: http.StatusBadRequest, - }, - { - name: "err_svc_invalid_timezone_format", - reqBody: formatReqBody(invalidTimezone, "", nil), - wantStatus: http.StatusBadRequest, - }, - { - name: "err_repo_not_found", - reqBody: formatReqBody(validTimezone, "", nil), - wantStatus: http.StatusNotFound, - mockArgs: &mockArgs{ - req: types.UpdateUserProfileRequest{ - UserName: userName, - Timezone: &validTimezone, - }, - err: types.ErrNotFound, - }, - }, - { - name: "err_repo_random", - reqBody: formatReqBody(validTimezone, "", nil), - wantStatus: http.StatusInternalServerError, - mockArgs: &mockArgs{ - req: types.UpdateUserProfileRequest{ - UserName: userName, - Timezone: &validTimezone, - }, - err: errors.New("random repo err"), + err: errSomethingWrong, }, }, } + for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := newUserProfilesTestData(t) - req := httptest.NewRequest(http.MethodPatch, "/userprofile/v1/profile", strings.NewReader(tt.reqBody)) + api, mockedSvc := setupTestAPI(t) if tt.mockArgs != nil { - mockedRepo.EXPECT().Update(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.err).Times(1) - } - if !tt.noUser { - req = req.WithContext(context.WithValue(req.Context(), types.UserKey{}, userName)) + mockedSvc.EXPECT(). + UpdateUserProfile(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.err). + Times(1) } - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveUpdateUserProfile, - WantStatus: tt.wantStatus, + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[updateUserProfileRequest, struct{}]{ + Method: http.MethodPatch, + Target: "/userprofile/v1/profile", + Req: tt.req, + Handler: withUser(api.serveUpdateUserProfile, userName), + NoResp: true, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/userprofile/v1/test/data.go b/internal/api/userprofile/v1/test/data.go deleted file mode 100644 index 9df8edc..0000000 --- a/internal/api/userprofile/v1/test/data.go +++ /dev/null @@ -1,34 +0,0 @@ -package test - -import ( - "testing" - - "go.uber.org/mock/gomock" - - "github.com/ozontech/seq-ui/internal/api/profiles" - repo "github.com/ozontech/seq-ui/internal/pkg/repository" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository/mock" - "github.com/ozontech/seq-ui/internal/pkg/service" -) - -func NewUserProfilesData(t *testing.T) (*repo_mock.MockUserProfiles, service.Service, *profiles.Profiles) { - ctl := gomock.NewController(t) - mockedRepo := repo_mock.NewMockUserProfiles(ctl) - r := &repo.Repository{ - UserProfiles: mockedRepo, - } - s := service.New(r) - p := profiles.New(s) - return mockedRepo, s, p -} - -func NewFavoriteQueriesTestData(t *testing.T) (*repo_mock.MockFavoriteQueries, service.Service, *profiles.Profiles) { - ctl := gomock.NewController(t) - mockedRepo := repo_mock.NewMockFavoriteQueries(ctl) - r := &repo.Repository{ - FavoriteQueries: mockedRepo, - } - s := service.New(r) - p := profiles.New(s) - return mockedRepo, s, p -} diff --git a/internal/api/userprofile/v1/userprofile.go b/internal/api/userprofile/v1/userprofile.go index 66f5320..c74a46a 100644 --- a/internal/api/userprofile/v1/userprofile.go +++ b/internal/api/userprofile/v1/userprofile.go @@ -3,10 +3,9 @@ package userprofile_v1 import ( "github.com/go-chi/chi/v5" - "github.com/ozontech/seq-ui/internal/api/profiles" grpc_api "github.com/ozontech/seq-ui/internal/api/userprofile/v1/grpc" http_api "github.com/ozontech/seq-ui/internal/api/userprofile/v1/http" - "github.com/ozontech/seq-ui/internal/pkg/service" + "github.com/ozontech/seq-ui/internal/pkg/service/userprofile" ) type UserProfile struct { @@ -14,10 +13,10 @@ type UserProfile struct { httpAPI *http_api.API } -func New(svc service.Service, p *profiles.Profiles) *UserProfile { +func New(svc userprofile.Service) *UserProfile { return &UserProfile{ - grpcAPI: grpc_api.New(svc, p), - httpAPI: http_api.New(svc, p), + grpcAPI: grpc_api.New(svc), + httpAPI: http_api.New(svc), } } diff --git a/internal/app/auth/oidc.go b/internal/app/auth/oidc.go index 4b9fdbd..15caf35 100644 --- a/internal/app/auth/oidc.go +++ b/internal/app/auth/oidc.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "net/http" + "slices" "time" "github.com/coreos/go-oidc/v3/oidc" @@ -169,10 +170,8 @@ func (p *oidcProvider) checkClients(clients []string) error { } for _, client := range clients { - for _, allowedClient := range p.allowedClients { - if client == allowedClient { - return nil - } + if slices.Contains(p.allowedClients, client) { + return nil } } diff --git a/internal/pkg/service/async_searches/mock/service.go b/internal/pkg/service/async_searches/mock/service.go new file mode 100644 index 0000000..4349018 --- /dev/null +++ b/internal/pkg/service/async_searches/mock/service.go @@ -0,0 +1,117 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ozontech/seq-ui/internal/pkg/service/async_searches (interfaces: Service) +// +// Generated by this command: +// +// mockgen -destination=internal/pkg/service/async_searches/mock/service.go github.com/ozontech/seq-ui/internal/pkg/service/async_searches Service +// + +// Package mock_asyncsearches is a generated GoMock package. +package mock_asyncsearches + +import ( + context "context" + reflect "reflect" + + seqapi "github.com/ozontech/seq-ui/pkg/seqapi/v1" + gomock "go.uber.org/mock/gomock" +) + +// MockService is a mock of Service interface. +type MockService struct { + ctrl *gomock.Controller + recorder *MockServiceMockRecorder + isgomock struct{} +} + +// MockServiceMockRecorder is the mock recorder for MockService. +type MockServiceMockRecorder struct { + mock *MockService +} + +// NewMockService creates a new mock instance. +func NewMockService(ctrl *gomock.Controller) *MockService { + mock := &MockService{ctrl: ctrl} + mock.recorder = &MockServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockService) EXPECT() *MockServiceMockRecorder { + return m.recorder +} + +// CancelAsyncSearch mocks base method. +func (m *MockService) CancelAsyncSearch(arg0 context.Context, arg1 *seqapi.CancelAsyncSearchRequest) (*seqapi.CancelAsyncSearchResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CancelAsyncSearch", arg0, arg1) + ret0, _ := ret[0].(*seqapi.CancelAsyncSearchResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CancelAsyncSearch indicates an expected call of CancelAsyncSearch. +func (mr *MockServiceMockRecorder) CancelAsyncSearch(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelAsyncSearch", reflect.TypeOf((*MockService)(nil).CancelAsyncSearch), arg0, arg1) +} + +// DeleteAsyncSearch mocks base method. +func (m *MockService) DeleteAsyncSearch(arg0 context.Context, arg1 *seqapi.DeleteAsyncSearchRequest) (*seqapi.DeleteAsyncSearchResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAsyncSearch", arg0, arg1) + ret0, _ := ret[0].(*seqapi.DeleteAsyncSearchResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteAsyncSearch indicates an expected call of DeleteAsyncSearch. +func (mr *MockServiceMockRecorder) DeleteAsyncSearch(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAsyncSearch", reflect.TypeOf((*MockService)(nil).DeleteAsyncSearch), arg0, arg1) +} + +// FetchAsyncSearchResult mocks base method. +func (m *MockService) FetchAsyncSearchResult(arg0 context.Context, arg1 *seqapi.FetchAsyncSearchResultRequest) (*seqapi.FetchAsyncSearchResultResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchAsyncSearchResult", arg0, arg1) + ret0, _ := ret[0].(*seqapi.FetchAsyncSearchResultResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchAsyncSearchResult indicates an expected call of FetchAsyncSearchResult. +func (mr *MockServiceMockRecorder) FetchAsyncSearchResult(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchAsyncSearchResult", reflect.TypeOf((*MockService)(nil).FetchAsyncSearchResult), arg0, arg1) +} + +// GetAsyncSearchesList mocks base method. +func (m *MockService) GetAsyncSearchesList(arg0 context.Context, arg1 *seqapi.GetAsyncSearchesListRequest) (*seqapi.GetAsyncSearchesListResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAsyncSearchesList", arg0, arg1) + ret0, _ := ret[0].(*seqapi.GetAsyncSearchesListResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAsyncSearchesList indicates an expected call of GetAsyncSearchesList. +func (mr *MockServiceMockRecorder) GetAsyncSearchesList(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAsyncSearchesList", reflect.TypeOf((*MockService)(nil).GetAsyncSearchesList), arg0, arg1) +} + +// StartAsyncSearch mocks base method. +func (m *MockService) StartAsyncSearch(arg0 context.Context, arg1 *seqapi.StartAsyncSearchRequest) (*seqapi.StartAsyncSearchResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StartAsyncSearch", arg0, arg1) + ret0, _ := ret[0].(*seqapi.StartAsyncSearchResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StartAsyncSearch indicates an expected call of StartAsyncSearch. +func (mr *MockServiceMockRecorder) StartAsyncSearch(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartAsyncSearch", reflect.TypeOf((*MockService)(nil).StartAsyncSearch), arg0, arg1) +} diff --git a/internal/pkg/service/async_searches/service.go b/internal/pkg/service/async_searches/service.go index 0960785..d6f4825 100644 --- a/internal/pkg/service/async_searches/service.go +++ b/internal/pkg/service/async_searches/service.go @@ -14,6 +14,7 @@ import ( "github.com/ozontech/seq-ui/internal/app/types" "github.com/ozontech/seq-ui/internal/pkg/client/seqdb" "github.com/ozontech/seq-ui/internal/pkg/repository" + "github.com/ozontech/seq-ui/internal/pkg/service/profiles" "github.com/ozontech/seq-ui/logger" "github.com/ozontech/seq-ui/metric" "github.com/ozontech/seq-ui/pkg/seqapi/v1" @@ -26,20 +27,23 @@ const ( deleteExpiredAsyncSearchesInterval = 1 * time.Minute ) -type Service struct { +type Service interface { + StartAsyncSearch(context.Context, *seqapi.StartAsyncSearchRequest) (*seqapi.StartAsyncSearchResponse, error) + DeleteAsyncSearch(context.Context, *seqapi.DeleteAsyncSearchRequest) (*seqapi.DeleteAsyncSearchResponse, error) + CancelAsyncSearch(context.Context, *seqapi.CancelAsyncSearchRequest) (*seqapi.CancelAsyncSearchResponse, error) + FetchAsyncSearchResult(context.Context, *seqapi.FetchAsyncSearchResultRequest) (*seqapi.FetchAsyncSearchResultResponse, error) + GetAsyncSearchesList(context.Context, *seqapi.GetAsyncSearchesListRequest) (*seqapi.GetAsyncSearchesListResponse, error) +} + +type service struct { repo repository.AsyncSearches seqDB seqdb.Client cfg config.AsyncSearch } -func New( - ctx context.Context, - repo repository.AsyncSearches, - seqDB seqdb.Client, - cfg config.AsyncSearch, -) *Service { - s := &Service{ +func New(ctx context.Context, repo repository.AsyncSearches, seqDB seqdb.Client, cfg config.AsyncSearch) Service { + s := &service{ repo: repo, seqDB: seqDB, cfg: cfg, @@ -50,11 +54,12 @@ func New( return s } -func (s *Service) StartAsyncSearch( - ctx context.Context, - ownerID int64, - req *seqapi.StartAsyncSearchRequest, -) (*seqapi.StartAsyncSearchResponse, error) { +func (s *service) StartAsyncSearch(ctx context.Context, req *seqapi.StartAsyncSearchRequest) (*seqapi.StartAsyncSearchResponse, error) { + ownerID, err := profiles.GetIDFromContext(ctx) + if err != nil { + return nil, err + } + if utf8.RuneCountInString(req.Query) > s.cfg.ListQueryLengthLimit { metric.AsyncSearchQueryTooLong.Inc() } @@ -82,11 +87,12 @@ func (s *Service) StartAsyncSearch( return resp, nil } -func (s *Service) DeleteAsyncSearch( - ctx context.Context, - ownerID int64, - req *seqapi.DeleteAsyncSearchRequest, -) (*seqapi.DeleteAsyncSearchResponse, error) { +func (s *service) DeleteAsyncSearch(ctx context.Context, req *seqapi.DeleteAsyncSearchRequest) (*seqapi.DeleteAsyncSearchResponse, error) { + ownerID, err := profiles.GetIDFromContext(ctx) + if err != nil { + return nil, err + } + searchInfo, err := s.repo.GetAsyncSearchById(ctx, req.SearchId) if err != nil { return nil, fmt.Errorf("failed to get async search by id: %w", err) @@ -115,11 +121,12 @@ func (s *Service) DeleteAsyncSearch( return resp, nil } -func (s *Service) CancelAsyncSearch( - ctx context.Context, - ownerID int64, - req *seqapi.CancelAsyncSearchRequest, -) (*seqapi.CancelAsyncSearchResponse, error) { +func (s *service) CancelAsyncSearch(ctx context.Context, req *seqapi.CancelAsyncSearchRequest) (*seqapi.CancelAsyncSearchResponse, error) { + ownerID, err := profiles.GetIDFromContext(ctx) + if err != nil { + return nil, err + } + searchInfo, err := s.repo.GetAsyncSearchById(ctx, req.SearchId) if err != nil { return nil, fmt.Errorf("failed to get async search by id: %w", err) @@ -137,10 +144,7 @@ func (s *Service) CancelAsyncSearch( return resp, nil } -func (s *Service) FetchAsyncSearchResult( - ctx context.Context, - req *seqapi.FetchAsyncSearchResultRequest, -) (*seqapi.FetchAsyncSearchResultResponse, error) { +func (s *service) FetchAsyncSearchResult(ctx context.Context, req *seqapi.FetchAsyncSearchResultRequest) (*seqapi.FetchAsyncSearchResultResponse, error) { searchInfo, err := s.repo.GetAsyncSearchById(ctx, req.SearchId) if err != nil { return nil, fmt.Errorf("failed to get async search by id: %w", err) @@ -156,10 +160,7 @@ func (s *Service) FetchAsyncSearchResult( return resp, nil } -func (s *Service) GetAsyncSearchesList( - ctx context.Context, - req *seqapi.GetAsyncSearchesListRequest, -) (*seqapi.GetAsyncSearchesListResponse, error) { +func (s *service) GetAsyncSearchesList(ctx context.Context, req *seqapi.GetAsyncSearchesListRequest) (*seqapi.GetAsyncSearchesListResponse, error) { searches, err := s.repo.GetAsyncSearchesList(ctx, types.GetAsyncSearchesListRequest{ Owner: req.OwnerName, }) @@ -191,7 +192,7 @@ func (s *Service) GetAsyncSearchesList( return resp, nil } -func (s *Service) isAdmin(ctx context.Context) bool { +func (s *service) isAdmin(ctx context.Context) bool { userName, err := types.GetUserKey(ctx) if err != nil { return false @@ -200,7 +201,7 @@ func (s *Service) isAdmin(ctx context.Context) bool { return slices.Index(s.cfg.AdminUsers, userName) >= 0 } -func (s *Service) deleteExpiredAsyncSearches(ctx context.Context) { +func (s *service) deleteExpiredAsyncSearches(ctx context.Context) { ticker := time.NewTicker(deleteExpiredAsyncSearchesInterval) defer ticker.Stop() @@ -217,7 +218,7 @@ func (s *Service) deleteExpiredAsyncSearches(ctx context.Context) { } } -func (s *Service) trimQueryToLimit(query string, limit int) string { +func (s *service) trimQueryToLimit(query string, limit int) string { count := 0 for i := range query { if count == limit { diff --git a/internal/pkg/service/dashboards/mock/service.go b/internal/pkg/service/dashboards/mock/service.go new file mode 100644 index 0000000..31e43b8 --- /dev/null +++ b/internal/pkg/service/dashboards/mock/service.go @@ -0,0 +1,145 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ozontech/seq-ui/internal/pkg/service/dashboards (interfaces: Service) +// +// Generated by this command: +// +// mockgen -destination=internal/pkg/service/dashboards/mock/service.go github.com/ozontech/seq-ui/internal/pkg/service/dashboards Service +// + +// Package mock_dashboards is a generated GoMock package. +package mock_dashboards + +import ( + context "context" + reflect "reflect" + + types "github.com/ozontech/seq-ui/internal/app/types" + gomock "go.uber.org/mock/gomock" +) + +// MockService is a mock of Service interface. +type MockService struct { + ctrl *gomock.Controller + recorder *MockServiceMockRecorder + isgomock struct{} +} + +// MockServiceMockRecorder is the mock recorder for MockService. +type MockServiceMockRecorder struct { + mock *MockService +} + +// NewMockService creates a new mock instance. +func NewMockService(ctrl *gomock.Controller) *MockService { + mock := &MockService{ctrl: ctrl} + mock.recorder = &MockServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockService) EXPECT() *MockServiceMockRecorder { + return m.recorder +} + +// CreateDashboard mocks base method. +func (m *MockService) CreateDashboard(arg0 context.Context, arg1 types.CreateDashboardRequest) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateDashboard", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateDashboard indicates an expected call of CreateDashboard. +func (mr *MockServiceMockRecorder) CreateDashboard(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDashboard", reflect.TypeOf((*MockService)(nil).CreateDashboard), arg0, arg1) +} + +// DeleteDashboard mocks base method. +func (m *MockService) DeleteDashboard(arg0 context.Context, arg1 types.DeleteDashboardRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteDashboard", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteDashboard indicates an expected call of DeleteDashboard. +func (mr *MockServiceMockRecorder) DeleteDashboard(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDashboard", reflect.TypeOf((*MockService)(nil).DeleteDashboard), arg0, arg1) +} + +// GetAllDashboards mocks base method. +func (m *MockService) GetAllDashboards(arg0 context.Context, arg1 types.GetAllDashboardsRequest) (types.DashboardInfosWithOwner, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllDashboards", arg0, arg1) + ret0, _ := ret[0].(types.DashboardInfosWithOwner) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllDashboards indicates an expected call of GetAllDashboards. +func (mr *MockServiceMockRecorder) GetAllDashboards(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllDashboards", reflect.TypeOf((*MockService)(nil).GetAllDashboards), arg0, arg1) +} + +// GetDashboardByUUID mocks base method. +func (m *MockService) GetDashboardByUUID(arg0 context.Context, arg1 string) (types.Dashboard, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDashboardByUUID", arg0, arg1) + ret0, _ := ret[0].(types.Dashboard) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDashboardByUUID indicates an expected call of GetDashboardByUUID. +func (mr *MockServiceMockRecorder) GetDashboardByUUID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDashboardByUUID", reflect.TypeOf((*MockService)(nil).GetDashboardByUUID), arg0, arg1) +} + +// GetMyDashboards mocks base method. +func (m *MockService) GetMyDashboards(arg0 context.Context, arg1 types.GetUserDashboardsRequest) (types.DashboardInfos, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMyDashboards", arg0, arg1) + ret0, _ := ret[0].(types.DashboardInfos) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMyDashboards indicates an expected call of GetMyDashboards. +func (mr *MockServiceMockRecorder) GetMyDashboards(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMyDashboards", reflect.TypeOf((*MockService)(nil).GetMyDashboards), arg0, arg1) +} + +// SearchDashboards mocks base method. +func (m *MockService) SearchDashboards(arg0 context.Context, arg1 types.SearchDashboardsRequest) (types.DashboardInfosWithOwner, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchDashboards", arg0, arg1) + ret0, _ := ret[0].(types.DashboardInfosWithOwner) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchDashboards indicates an expected call of SearchDashboards. +func (mr *MockServiceMockRecorder) SearchDashboards(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchDashboards", reflect.TypeOf((*MockService)(nil).SearchDashboards), arg0, arg1) +} + +// UpdateDashboard mocks base method. +func (m *MockService) UpdateDashboard(arg0 context.Context, arg1 types.UpdateDashboardRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateDashboard", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateDashboard indicates an expected call of UpdateDashboard. +func (mr *MockServiceMockRecorder) UpdateDashboard(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDashboard", reflect.TypeOf((*MockService)(nil).UpdateDashboard), arg0, arg1) +} diff --git a/internal/pkg/service/dashboards.go b/internal/pkg/service/dashboards/service.go similarity index 50% rename from internal/pkg/service/dashboards.go rename to internal/pkg/service/dashboards/service.go index e87b391..c84bd69 100644 --- a/internal/pkg/service/dashboards.go +++ b/internal/pkg/service/dashboards/service.go @@ -1,4 +1,4 @@ -package service +package dashboards import ( "context" @@ -6,37 +6,77 @@ import ( "github.com/gofrs/uuid" "github.com/ozontech/seq-ui/internal/app/types" + "github.com/ozontech/seq-ui/internal/pkg/repository" + "github.com/ozontech/seq-ui/internal/pkg/service/profiles" ) -// GetAllDashboards from underlying repository. +type Service interface { + GetAllDashboards(context.Context, types.GetAllDashboardsRequest) (types.DashboardInfosWithOwner, error) + GetMyDashboards(context.Context, types.GetUserDashboardsRequest) (types.DashboardInfos, error) + GetDashboardByUUID(context.Context, string) (types.Dashboard, error) + CreateDashboard(context.Context, types.CreateDashboardRequest) (string, error) + UpdateDashboard(context.Context, types.UpdateDashboardRequest) error + DeleteDashboard(context.Context, types.DeleteDashboardRequest) error + SearchDashboards(context.Context, types.SearchDashboardsRequest) (types.DashboardInfosWithOwner, error) +} + +type service struct { + repo repository.Dashboards +} + +func New(repo repository.Dashboards) Service { + return &service{ + repo: repo, + } +} + func (s *service) GetAllDashboards(ctx context.Context, req types.GetAllDashboardsRequest) (types.DashboardInfosWithOwner, error) { + // check auth and create profile if its doesn't exist + if _, err := profiles.GetIDFromContext(ctx); err != nil { + return nil, err + } + if err := checkLimitOffset(req.Limit, req.Offset); err != nil { return nil, err } - return s.repo.Dashboards.GetAll(ctx, req) + return s.repo.GetAll(ctx, req) } -// GetMyDashboards from underlying repository. func (s *service) GetMyDashboards(ctx context.Context, req types.GetUserDashboardsRequest) (types.DashboardInfos, error) { + profileID, err := profiles.GetIDFromContext(ctx) + if err != nil { + return nil, err + } + req.ProfileID = profileID + if err := checkLimitOffset(req.Limit, req.Offset); err != nil { return nil, err } - return s.repo.Dashboards.GetMy(ctx, req) + return s.repo.GetMy(ctx, req) } -// GetDashboardByUUID from underlying repository. func (s *service) GetDashboardByUUID(ctx context.Context, id string) (types.Dashboard, error) { + // check auth and create profile if its doesn't exist + if _, err := profiles.GetIDFromContext(ctx); err != nil { + return types.Dashboard{}, err + } + if err := checkUUID(id); err != nil { return types.Dashboard{}, err } - return s.repo.Dashboards.GetByUUID(ctx, id) + return s.repo.GetByUUID(ctx, id) } -// CreateDashboard in underlying repository. func (s *service) CreateDashboard(ctx context.Context, req types.CreateDashboardRequest) (string, error) { + profileID, err := profiles.GetIDFromContext(ctx) + if err != nil { + return "", err + } + req.ProfileID = profileID + if req.Name == "" { return "", types.NewErrInvalidRequestField("empty 'name'") } @@ -44,11 +84,16 @@ func (s *service) CreateDashboard(ctx context.Context, req types.CreateDashboard return "", types.NewErrInvalidRequestField("empty 'meta'") } - return s.repo.Dashboards.Create(ctx, req) + return s.repo.Create(ctx, req) } -// UpdateDashboard in underlying repository. func (s *service) UpdateDashboard(ctx context.Context, req types.UpdateDashboardRequest) error { + profileID, err := profiles.GetIDFromContext(ctx) + if err != nil { + return err + } + req.ProfileID = profileID + if err := checkUUID(req.UUID); err != nil { return err } @@ -56,24 +101,34 @@ func (s *service) UpdateDashboard(ctx context.Context, req types.UpdateDashboard return types.ErrEmptyUpdateRequest } - return s.repo.Dashboards.Update(ctx, req) + return s.repo.Update(ctx, req) } -// DeleteDashboard in underlying repository. func (s *service) DeleteDashboard(ctx context.Context, req types.DeleteDashboardRequest) error { + profileID, err := profiles.GetIDFromContext(ctx) + if err != nil { + return err + } + req.ProfileID = profileID + if err := checkUUID(req.UUID); err != nil { return err } - return s.repo.Dashboards.Delete(ctx, req) + return s.repo.Delete(ctx, req) } -// SearchDashboards in underlying repository. func (s *service) SearchDashboards(ctx context.Context, req types.SearchDashboardsRequest) (types.DashboardInfosWithOwner, error) { + // check auth and create profile if its doesn't exist + if _, err := profiles.GetIDFromContext(ctx); err != nil { + return nil, err + } + if err := checkLimitOffset(req.Limit, req.Offset); err != nil { return nil, err } - return s.repo.Dashboards.Search(ctx, req) + + return s.repo.Search(ctx, req) } func checkUUID(v string) error { diff --git a/internal/pkg/service/favorite_queries.go b/internal/pkg/service/favorite_queries.go deleted file mode 100644 index 9da4b5e..0000000 --- a/internal/pkg/service/favorite_queries.go +++ /dev/null @@ -1,30 +0,0 @@ -package service - -import ( - "context" - - "github.com/ozontech/seq-ui/internal/app/types" -) - -// GetFavoriteQueries from underlying repository. -func (s *service) GetFavoriteQueries(ctx context.Context, req types.GetFavoriteQueriesRequest) (types.FavoriteQueries, error) { - return s.repo.FavoriteQueries.GetAll(ctx, req) -} - -// GetOrCreateFavoriteQuery in underlying repository. -func (s *service) GetOrCreateFavoriteQuery(ctx context.Context, req types.GetOrCreateFavoriteQueryRequest) (int64, error) { - if req.Query == "" { - return -1, types.NewErrInvalidRequestField("empty query") - } - - return s.repo.FavoriteQueries.GetOrCreate(ctx, req) -} - -// DeleteFavoriteQuery in underlying repository. -func (s *service) DeleteFavoriteQuery(ctx context.Context, req types.DeleteFavoriteQueryRequest) error { - if req.ID <= 0 { - return types.NewErrInvalidRequestField("invalid id") - } - - return s.repo.FavoriteQueries.Delete(ctx, req) -} diff --git a/internal/pkg/service/massexport/mock/service.go b/internal/pkg/service/massexport/mock/service.go new file mode 100644 index 0000000..02928a0 --- /dev/null +++ b/internal/pkg/service/massexport/mock/service.go @@ -0,0 +1,115 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ozontech/seq-ui/internal/pkg/service/massexport (interfaces: Service) +// +// Generated by this command: +// +// mockgen -destination=internal/pkg/service/massexport/mock/service.go github.com/ozontech/seq-ui/internal/pkg/service/massexport Service +// + +// Package mock_massexport is a generated GoMock package. +package mock_massexport + +import ( + context "context" + reflect "reflect" + + types "github.com/ozontech/seq-ui/internal/app/types" + gomock "go.uber.org/mock/gomock" +) + +// MockService is a mock of Service interface. +type MockService struct { + ctrl *gomock.Controller + recorder *MockServiceMockRecorder + isgomock struct{} +} + +// MockServiceMockRecorder is the mock recorder for MockService. +type MockServiceMockRecorder struct { + mock *MockService +} + +// NewMockService creates a new mock instance. +func NewMockService(ctrl *gomock.Controller) *MockService { + mock := &MockService{ctrl: ctrl} + mock.recorder = &MockServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockService) EXPECT() *MockServiceMockRecorder { + return m.recorder +} + +// CancelExport mocks base method. +func (m *MockService) CancelExport(ctx context.Context, sessionID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CancelExport", ctx, sessionID) + ret0, _ := ret[0].(error) + return ret0 +} + +// CancelExport indicates an expected call of CancelExport. +func (mr *MockServiceMockRecorder) CancelExport(ctx, sessionID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelExport", reflect.TypeOf((*MockService)(nil).CancelExport), ctx, sessionID) +} + +// CheckExport mocks base method. +func (m *MockService) CheckExport(ctx context.Context, sessionID string) (types.ExportInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckExport", ctx, sessionID) + ret0, _ := ret[0].(types.ExportInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CheckExport indicates an expected call of CheckExport. +func (mr *MockServiceMockRecorder) CheckExport(ctx, sessionID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckExport", reflect.TypeOf((*MockService)(nil).CheckExport), ctx, sessionID) +} + +// GetAll mocks base method. +func (m *MockService) GetAll(ctx context.Context) ([]types.ExportInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAll", ctx) + ret0, _ := ret[0].([]types.ExportInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAll indicates an expected call of GetAll. +func (mr *MockServiceMockRecorder) GetAll(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockService)(nil).GetAll), ctx) +} + +// RestoreExport mocks base method. +func (m *MockService) RestoreExport(ctx context.Context, sessionID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RestoreExport", ctx, sessionID) + ret0, _ := ret[0].(error) + return ret0 +} + +// RestoreExport indicates an expected call of RestoreExport. +func (mr *MockServiceMockRecorder) RestoreExport(ctx, sessionID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreExport", reflect.TypeOf((*MockService)(nil).RestoreExport), ctx, sessionID) +} + +// StartExport mocks base method. +func (m *MockService) StartExport(ctx context.Context, req types.StartExportRequest) (types.StartExportResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StartExport", ctx, req) + ret0, _ := ret[0].(types.StartExportResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StartExport indicates an expected call of StartExport. +func (mr *MockServiceMockRecorder) StartExport(ctx, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartExport", reflect.TypeOf((*MockService)(nil).StartExport), ctx, req) +} diff --git a/internal/api/profiles/profiles.go b/internal/pkg/service/profiles/profiles.go similarity index 58% rename from internal/api/profiles/profiles.go rename to internal/pkg/service/profiles/profiles.go index 616a966..829bf4a 100644 --- a/internal/api/profiles/profiles.go +++ b/internal/pkg/service/profiles/profiles.go @@ -5,30 +5,43 @@ import ( "sync" "github.com/ozontech/seq-ui/internal/app/types" - "github.com/ozontech/seq-ui/internal/pkg/service" ) -type Profiles struct { - idByName map[string]int64 // map UserName->UserProfileId - mx sync.RWMutex - - service service.Service +type userProfileService interface { + GetOrCreateUserProfile(context.Context, types.GetOrCreateUserProfileRequest) (types.UserProfile, error) } -func New(svc service.Service) *Profiles { - return &Profiles{ +var profile *profiles + +func InitProfiles(svc userProfileService) { + profile = &profiles{ idByName: make(map[string]int64), service: svc, } } -func (p *Profiles) GeIDFromContext(ctx context.Context) (int64, error) { +type profiles struct { + idByName map[string]int64 // map UserName->UserProfileId + mx sync.RWMutex + + service userProfileService +} + +func GetIDFromContext(ctx context.Context) (int64, error) { + return profile.getIDFromContext(ctx) +} + +func SetID(userName string, userProfileID int64) { + profile.setID(userName, userProfileID) +} + +func (p *profiles) getIDFromContext(ctx context.Context) (int64, error) { userName, err := types.GetUserKey(ctx) if err != nil { return 0, err } - id, err := p.GetID(userName) + id, err := p.getID(userName) if err != nil { return 0, err } @@ -36,7 +49,7 @@ func (p *Profiles) GeIDFromContext(ctx context.Context) (int64, error) { return id, nil } -func (p *Profiles) GetID(userName string) (int64, error) { +func (p *profiles) getID(userName string) (int64, error) { p.mx.RLock() id, ok := p.idByName[userName] p.mx.RUnlock() @@ -62,7 +75,7 @@ func (p *Profiles) GetID(userName string) (int64, error) { return id, nil } -func (p *Profiles) SetID(userName string, userProfileID int64) { +func (p *profiles) setID(userName string, userProfileID int64) { p.mx.RLock() _, ok := p.idByName[userName] p.mx.RUnlock() diff --git a/internal/pkg/service/service.go b/internal/pkg/service/service.go deleted file mode 100644 index 4a34733..0000000 --- a/internal/pkg/service/service.go +++ /dev/null @@ -1,35 +0,0 @@ -package service - -import ( - "context" - - "github.com/ozontech/seq-ui/internal/app/types" - "github.com/ozontech/seq-ui/internal/pkg/repository" -) - -type Service interface { - GetOrCreateUserProfile(context.Context, types.GetOrCreateUserProfileRequest) (types.UserProfile, error) - UpdateUserProfile(context.Context, types.UpdateUserProfileRequest) error - - GetFavoriteQueries(context.Context, types.GetFavoriteQueriesRequest) (types.FavoriteQueries, error) - GetOrCreateFavoriteQuery(context.Context, types.GetOrCreateFavoriteQueryRequest) (int64, error) - DeleteFavoriteQuery(context.Context, types.DeleteFavoriteQueryRequest) error - - GetAllDashboards(context.Context, types.GetAllDashboardsRequest) (types.DashboardInfosWithOwner, error) - GetMyDashboards(context.Context, types.GetUserDashboardsRequest) (types.DashboardInfos, error) - GetDashboardByUUID(context.Context, string) (types.Dashboard, error) - CreateDashboard(context.Context, types.CreateDashboardRequest) (string, error) - UpdateDashboard(context.Context, types.UpdateDashboardRequest) error - DeleteDashboard(context.Context, types.DeleteDashboardRequest) error - SearchDashboards(context.Context, types.SearchDashboardsRequest) (types.DashboardInfosWithOwner, error) -} - -type service struct { - repo *repository.Repository -} - -func New(repo *repository.Repository) Service { - return &service{ - repo: repo, - } -} diff --git a/internal/pkg/service/user_profiles.go b/internal/pkg/service/user_profiles.go deleted file mode 100644 index e36685d..0000000 --- a/internal/pkg/service/user_profiles.go +++ /dev/null @@ -1,27 +0,0 @@ -package service - -import ( - "context" - "time" - - "github.com/ozontech/seq-ui/internal/app/types" -) - -// GetOrCreateUserProfile from underlying repository. -func (s *service) GetOrCreateUserProfile(ctx context.Context, req types.GetOrCreateUserProfileRequest) (types.UserProfile, error) { - return s.repo.UserProfiles.GetOrCreate(ctx, req) -} - -// UpdateUserProfile in underlying repository. -func (s *service) UpdateUserProfile(ctx context.Context, req types.UpdateUserProfileRequest) error { - if req.IsEmpty() { - return types.ErrEmptyUpdateRequest - } - if req.Timezone != nil { - if _, err := time.LoadLocation(*req.Timezone); err != nil { - return types.NewErrInvalidRequestField("invalid timezone format") - } - } - - return s.repo.UserProfiles.Update(ctx, req) -} diff --git a/internal/pkg/service/userprofile/mock/service.go b/internal/pkg/service/userprofile/mock/service.go new file mode 100644 index 0000000..bc3ff80 --- /dev/null +++ b/internal/pkg/service/userprofile/mock/service.go @@ -0,0 +1,115 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ozontech/seq-ui/internal/pkg/service/userprofile (interfaces: Service) +// +// Generated by this command: +// +// mockgen -destination=internal/pkg/service/userprofile/mock/service.go github.com/ozontech/seq-ui/internal/pkg/service/userprofile Service +// + +// Package mock_userprofile is a generated GoMock package. +package mock_userprofile + +import ( + context "context" + reflect "reflect" + + types "github.com/ozontech/seq-ui/internal/app/types" + gomock "go.uber.org/mock/gomock" +) + +// MockService is a mock of Service interface. +type MockService struct { + ctrl *gomock.Controller + recorder *MockServiceMockRecorder + isgomock struct{} +} + +// MockServiceMockRecorder is the mock recorder for MockService. +type MockServiceMockRecorder struct { + mock *MockService +} + +// NewMockService creates a new mock instance. +func NewMockService(ctrl *gomock.Controller) *MockService { + mock := &MockService{ctrl: ctrl} + mock.recorder = &MockServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockService) EXPECT() *MockServiceMockRecorder { + return m.recorder +} + +// DeleteFavoriteQuery mocks base method. +func (m *MockService) DeleteFavoriteQuery(arg0 context.Context, arg1 types.DeleteFavoriteQueryRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteFavoriteQuery", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteFavoriteQuery indicates an expected call of DeleteFavoriteQuery. +func (mr *MockServiceMockRecorder) DeleteFavoriteQuery(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFavoriteQuery", reflect.TypeOf((*MockService)(nil).DeleteFavoriteQuery), arg0, arg1) +} + +// GetFavoriteQueries mocks base method. +func (m *MockService) GetFavoriteQueries(arg0 context.Context, arg1 types.GetFavoriteQueriesRequest) (types.FavoriteQueries, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFavoriteQueries", arg0, arg1) + ret0, _ := ret[0].(types.FavoriteQueries) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFavoriteQueries indicates an expected call of GetFavoriteQueries. +func (mr *MockServiceMockRecorder) GetFavoriteQueries(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFavoriteQueries", reflect.TypeOf((*MockService)(nil).GetFavoriteQueries), arg0, arg1) +} + +// GetOrCreateFavoriteQuery mocks base method. +func (m *MockService) GetOrCreateFavoriteQuery(arg0 context.Context, arg1 types.GetOrCreateFavoriteQueryRequest) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOrCreateFavoriteQuery", arg0, arg1) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOrCreateFavoriteQuery indicates an expected call of GetOrCreateFavoriteQuery. +func (mr *MockServiceMockRecorder) GetOrCreateFavoriteQuery(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrCreateFavoriteQuery", reflect.TypeOf((*MockService)(nil).GetOrCreateFavoriteQuery), arg0, arg1) +} + +// GetOrCreateUserProfile mocks base method. +func (m *MockService) GetOrCreateUserProfile(arg0 context.Context, arg1 types.GetOrCreateUserProfileRequest) (types.UserProfile, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOrCreateUserProfile", arg0, arg1) + ret0, _ := ret[0].(types.UserProfile) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOrCreateUserProfile indicates an expected call of GetOrCreateUserProfile. +func (mr *MockServiceMockRecorder) GetOrCreateUserProfile(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrCreateUserProfile", reflect.TypeOf((*MockService)(nil).GetOrCreateUserProfile), arg0, arg1) +} + +// UpdateUserProfile mocks base method. +func (m *MockService) UpdateUserProfile(arg0 context.Context, arg1 types.UpdateUserProfileRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserProfile", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateUserProfile indicates an expected call of UpdateUserProfile. +func (mr *MockServiceMockRecorder) UpdateUserProfile(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserProfile", reflect.TypeOf((*MockService)(nil).UpdateUserProfile), arg0, arg1) +} diff --git a/internal/pkg/service/userprofile/service.go b/internal/pkg/service/userprofile/service.go new file mode 100644 index 0000000..a4672cd --- /dev/null +++ b/internal/pkg/service/userprofile/service.go @@ -0,0 +1,92 @@ +package userprofile + +import ( + "context" + "time" + + "github.com/ozontech/seq-ui/internal/app/types" + "github.com/ozontech/seq-ui/internal/pkg/repository" + "github.com/ozontech/seq-ui/internal/pkg/service/profiles" +) + +type Service interface { + GetOrCreateUserProfile(context.Context, types.GetOrCreateUserProfileRequest) (types.UserProfile, error) + UpdateUserProfile(context.Context, types.UpdateUserProfileRequest) error + GetFavoriteQueries(context.Context, types.GetFavoriteQueriesRequest) (types.FavoriteQueries, error) + GetOrCreateFavoriteQuery(context.Context, types.GetOrCreateFavoriteQueryRequest) (int64, error) + DeleteFavoriteQuery(context.Context, types.DeleteFavoriteQueryRequest) error +} + +type service struct { + UserProfiles repository.UserProfiles + FavoriteQueries repository.FavoriteQueries +} + +func New(up repository.UserProfiles, fq repository.FavoriteQueries) Service { + return &service{ + UserProfiles: up, + FavoriteQueries: fq, + } +} + +func (s *service) GetOrCreateUserProfile(ctx context.Context, req types.GetOrCreateUserProfileRequest) (types.UserProfile, error) { + up, err := s.UserProfiles.GetOrCreate(ctx, req) + if err != nil { + return up, err + } + + profiles.SetID(req.UserName, up.ID) + + return up, nil +} + +func (s *service) UpdateUserProfile(ctx context.Context, req types.UpdateUserProfileRequest) error { + if req.IsEmpty() { + return types.ErrEmptyUpdateRequest + } + if req.Timezone != nil { + if _, err := time.LoadLocation(*req.Timezone); err != nil { + return types.NewErrInvalidRequestField("invalid timezone format") + } + } + + return s.UserProfiles.Update(ctx, req) +} + +func (s *service) GetFavoriteQueries(ctx context.Context, req types.GetFavoriteQueriesRequest) (types.FavoriteQueries, error) { + profileID, err := profiles.GetIDFromContext(ctx) + if err != nil { + return nil, err + } + req.ProfileID = profileID + + return s.FavoriteQueries.GetAll(ctx, req) +} + +func (s *service) GetOrCreateFavoriteQuery(ctx context.Context, req types.GetOrCreateFavoriteQueryRequest) (int64, error) { + profileID, err := profiles.GetIDFromContext(ctx) + if err != nil { + return 0, err + } + req.ProfileID = profileID + + if req.Query == "" { + return -1, types.NewErrInvalidRequestField("empty query") + } + + return s.FavoriteQueries.GetOrCreate(ctx, req) +} + +func (s *service) DeleteFavoriteQuery(ctx context.Context, req types.DeleteFavoriteQueryRequest) error { + profileID, err := profiles.GetIDFromContext(ctx) + if err != nil { + return err + } + req.ProfileID = profileID + + if req.ID <= 0 { + return types.NewErrInvalidRequestField("invalid id") + } + + return s.FavoriteQueries.Delete(ctx, req) +}