diff --git a/Makefile b/Makefile index f236185..af5143d 100644 --- a/Makefile +++ b/Makefile @@ -122,22 +122,6 @@ 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 bb3c7cc..9450352 100644 --- a/cmd/seq-ui/main.go +++ b/cmd/seq-ui/main.go @@ -21,6 +21,7 @@ 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" @@ -29,14 +30,12 @@ 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" ) @@ -152,23 +151,23 @@ func initApp(ctx context.Context, cfg config.Config) *api.Registrar { } var ( - asyncSearchesService asyncsearches.Service + asyncSearchesService *asyncsearches.Service + p *profiles.Profiles userProfileV1 *userprofile_v1.UserProfile dashboardsV1 *dashboards_v1.Dashboards ) if db != nil { repo := repository.New(db, cfg.Server.DB.RequestTimeout) - userProfilesSvc := userprofile.New(repo.UserProfiles, repo.FavoriteQueries) - dashboardsSvc := dashboards.New(repo.Dashboards) - profiles.InitProfiles(userProfilesSvc) + svc := service.New(repo) + p = profiles.New(svc) - userProfileV1 = userprofile_v1.New(userProfilesSvc) - dashboardsV1 = dashboards_v1.New(dashboardsSvc) + userProfileV1 = userprofile_v1.New(svc, p) + dashboardsV1 = dashboards_v1.New(svc, p) asyncSearchesService = asyncsearches.New(ctx, repo, defaultClient, cfg.Handlers.AsyncSearch) } - seqApiV1 := seqapi_v1.New(cfg.Handlers.SeqAPI, seqDBClients, inmemWithRedisCache, redisCache, asyncSearchesService) + seqApiV1 := seqapi_v1.New(cfg.Handlers.SeqAPI, seqDBClients, inmemWithRedisCache, redisCache, asyncSearchesService, p) 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 ec0031e..cdb62f1 100644 --- a/internal/api/dashboards/v1/dashboards.go +++ b/internal/api/dashboards/v1/dashboards.go @@ -5,7 +5,8 @@ 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/pkg/service/dashboards" + "github.com/ozontech/seq-ui/internal/api/profiles" + "github.com/ozontech/seq-ui/internal/pkg/service" ) type Dashboards struct { @@ -13,10 +14,10 @@ type Dashboards struct { httpAPI *http_api.API } -func New(svc dashboards.Service) *Dashboards { +func New(svc service.Service, p *profiles.Profiles) *Dashboards { return &Dashboards{ - grpcAPI: grpc_api.New(svc), - httpAPI: http_api.New(svc), + grpcAPI: grpc_api.New(svc, p), + httpAPI: http_api.New(svc, p), } } diff --git a/internal/api/dashboards/v1/grpc/api.go b/internal/api/dashboards/v1/grpc/api.go index 2422d0b..7e59b4e 100644 --- a/internal/api/dashboards/v1/grpc/api.go +++ b/internal/api/dashboards/v1/grpc/api.go @@ -1,18 +1,21 @@ package grpc import ( - "github.com/ozontech/seq-ui/internal/pkg/service/dashboards" - api "github.com/ozontech/seq-ui/pkg/dashboards/v1" + "github.com/ozontech/seq-ui/internal/api/profiles" + "github.com/ozontech/seq-ui/internal/pkg/service" + "github.com/ozontech/seq-ui/pkg/dashboards/v1" ) type API struct { - api.UnimplementedDashboardsServiceServer + dashboards.UnimplementedDashboardsServiceServer - service dashboards.Service + service service.Service + profiles *profiles.Profiles } -func New(svc dashboards.Service) *API { +func New(svc service.Service, p *profiles.Profiles) *API { return &API{ - service: svc, + service: svc, + profiles: p, } } diff --git a/internal/api/dashboards/v1/grpc/create.go b/internal/api/dashboards/v1/grpc/create.go index 66e44a0..b6f09cf 100644 --- a/internal/api/dashboards/v1/grpc/create.go +++ b/internal/api/dashboards/v1/grpc/create.go @@ -22,11 +22,16 @@ func (a *API) Create(ctx context.Context, req *dashboards.CreateRequest) (*dashb }, ) - request := types.CreateDashboardRequest{ - Name: req.Name, - Meta: req.Meta, + 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, + } 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 d143d57..30a5c25 100644 --- a/internal/api/dashboards/v1/grpc/create_test.go +++ b/internal/api/dashboards/v1/grpc/create_test.go @@ -2,6 +2,7 @@ package grpc import ( "context" + "errors" "testing" "github.com/stretchr/testify/require" @@ -14,6 +15,12 @@ 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 @@ -28,56 +35,82 @@ func TestCreate(t *testing.T) { wantCode codes.Code mockArgs *mockArgs + noUser bool }{ { - name: "ok", + name: "success", req: &dashboards.CreateRequest{ - Name: testDashboardName, - Meta: testDashboardMeta, + Name: dashboardName, + Meta: dashboardMeta, }, want: &dashboards.CreateResponse{ - Uuid: testDashboardUUID, + Uuid: dashboardUUID, }, wantCode: codes.OK, mockArgs: &mockArgs{ req: types.CreateDashboardRequest{ - Name: testDashboardName, - Meta: testDashboardMeta, + ProfileID: profileID, + Name: dashboardName, + Meta: dashboardMeta, }, - resp: testDashboardUUID, + resp: dashboardUUID, + }, + }, + { + name: "err_no_user", + wantCode: codes.Unauthenticated, + noUser: true, + }, + { + name: "err_svc_empty_name", + req: &dashboards.CreateRequest{ + Meta: dashboardMeta, }, + wantCode: codes.InvalidArgument, }, { - name: "err_svc", + name: "err_svc_empty_meta", req: &dashboards.CreateRequest{ - Name: testDashboardName, - Meta: testDashboardMeta, + Name: dashboardName, + }, + wantCode: codes.InvalidArgument, + }, + { + name: "err_repo_random", + req: &dashboards.CreateRequest{ + Name: dashboardName, + Meta: dashboardMeta, }, wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.CreateDashboardRequest{ - Name: testDashboardName, - Meta: testDashboardMeta, + ProfileID: profileID, + Name: dashboardName, + Meta: dashboardMeta, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newTestData(t) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - CreateDashboard(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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) } - got, err := api.Create(context.Background(), tt.req) + got, err := api.Create(ctx, 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 5d286a7..e2408f3 100644 --- a/internal/api/dashboards/v1/grpc/delete.go +++ b/internal/api/dashboards/v1/grpc/delete.go @@ -22,11 +22,16 @@ func (a *API) Delete(ctx context.Context, req *dashboards.DeleteRequest) (*dashb }, ) - request := types.DeleteDashboardRequest{ - UUID: req.Uuid, + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + return nil, grpcutil.ProcessError(err) } - if err := a.service.DeleteDashboard(ctx, request); err != nil { + request := types.DeleteDashboardRequest{ + UUID: req.Uuid, + ProfileID: profileID, + } + 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 8cd9a9c..9401f59 100644 --- a/internal/api/dashboards/v1/grpc/delete_test.go +++ b/internal/api/dashboards/v1/grpc/delete_test.go @@ -2,6 +2,7 @@ package grpc import ( "context" + "errors" "testing" "github.com/stretchr/testify/require" @@ -14,6 +15,10 @@ 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 @@ -27,49 +32,82 @@ func TestDelete(t *testing.T) { wantCode codes.Code mockArgs *mockArgs + noUser bool }{ { - name: "ok", + name: "success", req: &dashboards.DeleteRequest{ - Uuid: testDashboardUUID, + Uuid: dashboardUUID, }, want: &dashboards.DeleteResponse{}, wantCode: codes.OK, mockArgs: &mockArgs{ req: types.DeleteDashboardRequest{ - UUID: testDashboardUUID, + 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, }, + err: types.ErrPermissionDenied, }, }, { - name: "err_svc", + name: "err_repo_random", req: &dashboards.DeleteRequest{ - Uuid: testDashboardUUID, + Uuid: dashboardUUID, }, wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.DeleteDashboardRequest{ - UUID: testDashboardUUID, + UUID: dashboardUUID, + ProfileID: profileID, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newTestData(t) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - DeleteDashboard(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.err). - Times(1) + 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) } - got, err := api.Delete(context.Background(), tt.req) + got, err := api.Delete(ctx, 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 1ee7941..57ae765 100644 --- a/internal/api/dashboards/v1/grpc/get_all.go +++ b/internal/api/dashboards/v1/grpc/get_all.go @@ -26,11 +26,15 @@ 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 7878255..46e3f18 100644 --- a/internal/api/dashboards/v1/grpc/get_all_test.go +++ b/internal/api/dashboards/v1/grpc/get_all_test.go @@ -2,6 +2,7 @@ package grpc import ( "context" + "errors" "testing" "github.com/stretchr/testify/require" @@ -14,6 +15,9 @@ import ( ) func TestGetAll(t *testing.T) { + userName := "unnamed" + var profileID int64 = 1 + type mockArgs struct { req types.GetAllDashboardsRequest resp types.DashboardInfosWithOwner @@ -28,12 +32,13 @@ func TestGetAll(t *testing.T) { wantCode codes.Code mockArgs *mockArgs + noUser bool }{ { - name: "ok", + name: "success", req: &dashboards.GetAllRequest{ - Limit: int32(testLimit), - Offset: int32(testOffset), + Limit: 2, + Offset: 0, }, want: &dashboards.GetAllResponse{ Dashboards: []*dashboards.GetAllResponse_Dashboard{ @@ -44,8 +49,8 @@ func TestGetAll(t *testing.T) { wantCode: codes.OK, mockArgs: &mockArgs{ req: types.GetAllDashboardsRequest{ - Limit: testLimit, - Offset: testOffset, + Limit: 2, + Offset: 0, }, resp: types.DashboardInfosWithOwner{ { @@ -66,36 +71,61 @@ func TestGetAll(t *testing.T) { }, }, { - name: "err_svc", + 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", req: &dashboards.GetAllRequest{ - Limit: int32(testLimit), - Offset: int32(testOffset), + Limit: 2, + Offset: -10, + }, + wantCode: codes.InvalidArgument, + }, + { + name: "err_repo_random", + req: &dashboards.GetAllRequest{ + Limit: 2, + Offset: 0, }, wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.GetAllDashboardsRequest{ - Limit: testLimit, - Offset: testOffset, + Limit: 2, + Offset: 0, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newTestData(t) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - GetAllDashboards(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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) } - got, err := api.GetAll(context.Background(), tt.req) + got, err := api.GetAll(ctx, 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 91df1ab..0db45b1 100644 --- a/internal/api/dashboards/v1/grpc/get_by_uuid.go +++ b/internal/api/dashboards/v1/grpc/get_by_uuid.go @@ -21,6 +21,11 @@ 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 585b755..a4d4385 100644 --- a/internal/api/dashboards/v1/grpc/get_by_uuid_test.go +++ b/internal/api/dashboards/v1/grpc/get_by_uuid_test.go @@ -2,6 +2,7 @@ package grpc import ( "context" + "errors" "testing" "github.com/stretchr/testify/require" @@ -14,9 +15,13 @@ import ( ) func TestGetByUUID(t *testing.T) { - var ( - dashboardOwner = "owner" - ) + userName := "unnamed" + var profileID int64 = 1 + dashboardUUID := "064dc707-02b8-7000-8201-02a7f396738a" + dashboardName := "my_dashboard" + dashboardMeta := "my_meta" + dashboardOwner := "owner" + type mockArgs struct { uuid string resp types.Dashboard @@ -31,54 +36,82 @@ func TestGetByUUID(t *testing.T) { wantCode codes.Code mockArgs *mockArgs + noUser bool }{ { - name: "ok", + name: "success", req: &dashboards.GetByUUIDRequest{ - Uuid: testDashboardUUID, + Uuid: dashboardUUID, }, want: &dashboards.GetByUUIDResponse{ - Name: testDashboardName, - Meta: testDashboardMeta, + Name: dashboardName, + Meta: dashboardMeta, OwnerName: dashboardOwner, }, wantCode: codes.OK, mockArgs: &mockArgs{ - uuid: testDashboardUUID, + uuid: dashboardUUID, resp: types.Dashboard{ - Name: testDashboardName, - Meta: testDashboardMeta, + Name: dashboardName, + Meta: dashboardMeta, OwnerName: dashboardOwner, }, }, }, { - name: "err_svc", + 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", req: &dashboards.GetByUUIDRequest{ - Uuid: testDashboardUUID, + Uuid: dashboardUUID, }, wantCode: codes.Internal, mockArgs: &mockArgs{ - uuid: testDashboardUUID, - err: errSomethingWrong, + uuid: dashboardUUID, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newTestData(t) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - GetDashboardByUUID(gomock.Any(), tt.mockArgs.uuid). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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) } - got, err := api.GetByUUID(context.Background(), tt.req) + got, err := api.GetByUUID(ctx, 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 b65f0cb..d684019 100644 --- a/internal/api/dashboards/v1/grpc/get_my.go +++ b/internal/api/dashboards/v1/grpc/get_my.go @@ -26,11 +26,16 @@ func (a *API) GetMy(ctx context.Context, req *dashboards.GetMyRequest) (*dashboa }, ) - request := types.GetUserDashboardsRequest{ - Limit: int(req.Limit), - Offset: int(req.Offset), + 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), + } 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 cffaf3d..e0dda27 100644 --- a/internal/api/dashboards/v1/grpc/get_my_test.go +++ b/internal/api/dashboards/v1/grpc/get_my_test.go @@ -2,6 +2,7 @@ package grpc import ( "context" + "errors" "testing" "github.com/stretchr/testify/require" @@ -14,6 +15,9 @@ import ( ) func TestGetMy(t *testing.T) { + userName := "unnamed" + var profileID int64 = 1 + type mockArgs struct { req types.GetUserDashboardsRequest resp types.DashboardInfos @@ -28,12 +32,13 @@ func TestGetMy(t *testing.T) { wantCode codes.Code mockArgs *mockArgs + noUser bool }{ { - name: "ok", + name: "success", req: &dashboards.GetMyRequest{ - Limit: int32(testLimit), - Offset: int32(testOffset), + Limit: 2, + Offset: 0, }, want: &dashboards.GetMyResponse{ Dashboards: []*dashboards.GetMyResponse_Dashboard{ @@ -44,8 +49,9 @@ func TestGetMy(t *testing.T) { wantCode: codes.OK, mockArgs: &mockArgs{ req: types.GetUserDashboardsRequest{ - Limit: testLimit, - Offset: testOffset, + ProfileID: profileID, + Limit: 2, + Offset: 0, }, resp: types.DashboardInfos{ {UUID: "064dc707-02b8-7000-8201-02a7f396738a", Name: "dashboard1"}, @@ -54,36 +60,62 @@ func TestGetMy(t *testing.T) { }, }, { - name: "err_svc", + 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", req: &dashboards.GetMyRequest{ - Limit: int32(testLimit), - Offset: int32(testOffset), + Limit: 2, + Offset: -10, + }, + wantCode: codes.InvalidArgument, + }, + { + name: "err_repo_random", + req: &dashboards.GetMyRequest{ + Limit: 2, + Offset: 0, }, wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.GetUserDashboardsRequest{ - Limit: testLimit, - Offset: testOffset, + ProfileID: profileID, + Limit: 2, + Offset: 0, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newTestData(t) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - GetMyDashboards(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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) } - got, err := api.GetMy(context.Background(), tt.req) + got, err := api.GetMy(ctx, 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 a0bb271..45213fd 100644 --- a/internal/api/dashboards/v1/grpc/search.go +++ b/internal/api/dashboards/v1/grpc/search.go @@ -40,12 +40,16 @@ 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 2a9f032..4b08c62 100644 --- a/internal/api/dashboards/v1/grpc/search_test.go +++ b/internal/api/dashboards/v1/grpc/search_test.go @@ -2,6 +2,7 @@ package grpc import ( "context" + "errors" "testing" "github.com/stretchr/testify/require" @@ -14,9 +15,8 @@ import ( ) func TestSearch(t *testing.T) { - var ( - userName = "unnamed" - ) + userName := "unnamed" + var profileID int64 = 1 type mockArgs struct { req types.SearchDashboardsRequest @@ -32,13 +32,14 @@ func TestSearch(t *testing.T) { wantCode codes.Code mockArgs *mockArgs + noUser bool }{ { - name: "ok", + name: "success", req: &dashboards.SearchRequest{ Query: "test", - Limit: int32(testLimit), - Offset: int32(testOffset), + Limit: 2, + Offset: 0, }, want: &dashboards.SearchResponse{ Dashboards: []*dashboards.SearchResponse_Dashboard{ @@ -50,8 +51,8 @@ func TestSearch(t *testing.T) { mockArgs: &mockArgs{ req: types.SearchDashboardsRequest{ Query: "test", - Limit: testLimit, - Offset: testOffset, + Limit: 2, + Offset: 0, }, resp: types.DashboardInfosWithOwner{ { @@ -72,11 +73,11 @@ func TestSearch(t *testing.T) { }, }, { - name: "ok_with_filter", + name: "success_with_filter", req: &dashboards.SearchRequest{ Query: "test", - Limit: int32(testLimit), - Offset: int32(testOffset), + Limit: 2, + Offset: 0, Filter: &dashboards.SearchRequest_Filter{ OwnerName: &userName, }, @@ -90,8 +91,8 @@ func TestSearch(t *testing.T) { mockArgs: &mockArgs{ req: types.SearchDashboardsRequest{ Query: "test", - Limit: testLimit, - Offset: testOffset, + Limit: 2, + Offset: 0, Filter: &types.SearchDashboardsFilter{ OwnerName: &userName, }, @@ -108,38 +109,63 @@ func TestSearch(t *testing.T) { }, }, { - name: "err_svc", + 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", req: &dashboards.SearchRequest{ Query: "test", - Limit: int32(testLimit), - Offset: int32(testOffset), + Limit: 2, + Offset: 0, }, wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.SearchDashboardsRequest{ Query: "test", - Limit: testLimit, - Offset: testOffset, + Limit: 2, + Offset: 0, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newTestData(t) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - SearchDashboards(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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) } - got, err := api.Search(context.Background(), tt.req) + got, err := api.Search(ctx, 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 111c97c..d230b21 100644 --- a/internal/api/dashboards/v1/grpc/test_data.go +++ b/internal/api/dashboards/v1/grpc/test_data.go @@ -1,26 +1,13 @@ package grpc import ( - "errors" "testing" - "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 + "github.com/ozontech/seq-ui/internal/api/dashboards/v1/test" + repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository/mock" ) -func setupTestAPI(t *testing.T) (*API, *mock.MockService) { - ctrl := gomock.NewController(t) - mockedSvc := mock.NewMockService(ctrl) - return New(mockedSvc), mockedSvc +func newTestData(t *testing.T) (*API, *repo_mock.MockDashboards) { + mock, s, p := test.NewTestData(t) + return New(s, p), mock } diff --git a/internal/api/dashboards/v1/grpc/update.go b/internal/api/dashboards/v1/grpc/update.go index 3151157..f382c4d 100644 --- a/internal/api/dashboards/v1/grpc/update.go +++ b/internal/api/dashboards/v1/grpc/update.go @@ -26,13 +26,18 @@ func (a *API) Update(ctx context.Context, req *dashboards.UpdateRequest) (*dashb }, ) - request := types.UpdateDashboardRequest{ - UUID: req.Uuid, - Name: req.Name, - Meta: req.Meta, + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + return nil, grpcutil.ProcessError(err) } - if err := a.service.UpdateDashboard(ctx, request); err != nil { + request := types.UpdateDashboardRequest{ + UUID: req.Uuid, + ProfileID: profileID, + Name: req.Name, + Meta: req.Meta, + } + 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 578b5ed..1fa2e25 100644 --- a/internal/api/dashboards/v1/grpc/update_test.go +++ b/internal/api/dashboards/v1/grpc/update_test.go @@ -2,6 +2,7 @@ package grpc import ( "context" + "errors" "testing" "github.com/stretchr/testify/require" @@ -14,6 +15,12 @@ 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 @@ -27,57 +34,153 @@ func TestUpdate(t *testing.T) { wantCode codes.Code mockArgs *mockArgs + noUser bool }{ { - name: "ok", + name: "success_all", req: &dashboards.UpdateRequest{ - Uuid: testDashboardUUID, - Name: &testDashboardName, - Meta: &testDashboardMeta, + Uuid: dashboardUUID, + Name: &dashboardName, + Meta: &dashboardMeta, }, want: &dashboards.UpdateResponse{}, wantCode: codes.OK, mockArgs: &mockArgs{ req: types.UpdateDashboardRequest{ - UUID: testDashboardUUID, - Name: &testDashboardName, - Meta: &testDashboardMeta, + 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, }, + err: types.ErrPermissionDenied, }, }, { - name: "err_svc", + name: "err_repo_random", req: &dashboards.UpdateRequest{ - Uuid: testDashboardUUID, - Name: &testDashboardName, - Meta: &testDashboardMeta, + Uuid: dashboardUUID, + Name: &dashboardName, + Meta: &dashboardMeta, }, wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.UpdateDashboardRequest{ - UUID: testDashboardUUID, - Name: &testDashboardName, - Meta: &testDashboardMeta, + ProfileID: profileID, + UUID: dashboardUUID, + Name: &dashboardName, + Meta: &dashboardMeta, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newTestData(t) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - UpdateDashboard(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.err). - Times(1) + 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) } - got, err := api.Update(context.Background(), tt.req) + got, err := api.Update(ctx, 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 da629d0..b192e69 100644 --- a/internal/api/dashboards/v1/http/api.go +++ b/internal/api/dashboards/v1/http/api.go @@ -3,17 +3,20 @@ 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/dashboards" + "github.com/ozontech/seq-ui/internal/pkg/service" ) type API struct { - service dashboards.Service + service service.Service + profiles *profiles.Profiles } -func New(svc dashboards.Service) *API { +func New(svc service.Service, p *profiles.Profiles) *API { return &API{ - service: svc, + service: svc, + profiles: p, } } diff --git a/internal/api/dashboards/v1/http/create.go b/internal/api/dashboards/v1/http/create.go index fef7a33..70f7179 100644 --- a/internal/api/dashboards/v1/http/create.go +++ b/internal/api/dashboards/v1/http/create.go @@ -40,11 +40,17 @@ func (a *API) serveCreate(w http.ResponseWriter, r *http.Request) { }, ) - req := types.CreateDashboardRequest{ - Name: httpReq.Name, - Meta: httpReq.Meta, + 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, + } 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 ae791f3..1582042 100644 --- a/internal/api/dashboards/v1/http/create_test.go +++ b/internal/api/dashboards/v1/http/create_test.go @@ -1,7 +1,12 @@ package http import ( + "context" + "errors" + "fmt" "net/http" + "net/http/httptest" + "strings" "testing" "go.uber.org/mock/gomock" @@ -11,11 +16,15 @@ import ( ) func TestServeCreate(t *testing.T) { - var ( - dashboardUUID = "064dc707-02b8-7000-8201-02a7f396738a" - dashboardMeta = "my_meta" - dashboardName = "my_dashboard" - ) + 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) + } type mockArgs struct { req types.CreateDashboardRequest @@ -26,58 +35,85 @@ func TestServeCreate(t *testing.T) { tests := []struct { name string - req createRequest - want createResponse - wantErr bool + reqBody string + wantRespBody string + wantStatus int mockArgs *mockArgs + noUser bool }{ { - name: "ok", - req: createRequest{Name: dashboardName, Meta: dashboardMeta}, - want: createResponse{UUID: dashboardUUID}, + name: "success", + reqBody: formatReqBody(dashboardName, dashboardMeta), + wantRespBody: fmt.Sprintf(`{"uuid":%q}`, dashboardUUID), + wantStatus: http.StatusOK, mockArgs: &mockArgs{ req: types.CreateDashboardRequest{ - Name: dashboardName, - Meta: dashboardMeta, + ProfileID: profileID, + Name: dashboardName, + Meta: dashboardMeta, }, resp: dashboardUUID, }, }, { - name: "err_svc", - req: createRequest{Name: dashboardName, Meta: dashboardMeta}, - wantErr: true, + 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, mockArgs: &mockArgs{ req: types.CreateDashboardRequest{ - Name: dashboardName, - Meta: dashboardMeta, + ProfileID: profileID, + Name: dashboardName, + Meta: dashboardMeta, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newTestData(t) + req := httptest.NewRequest(http.MethodPost, "/dashboards/v1/", strings.NewReader(tt.reqBody)) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - CreateDashboard(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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) } - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveCreate, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } diff --git a/internal/api/dashboards/v1/http/delete.go b/internal/api/dashboards/v1/http/delete.go index 8a2c958..d7cfcc4 100644 --- a/internal/api/dashboards/v1/http/delete.go +++ b/internal/api/dashboards/v1/http/delete.go @@ -35,11 +35,17 @@ func (a *API) serveDelete(w http.ResponseWriter, r *http.Request) { }, ) - req := types.DeleteDashboardRequest{ - UUID: uuid, + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + httputil.ProcessError(wr, err) + return } - err := a.service.DeleteDashboard(ctx, req) + req := types.DeleteDashboardRequest{ + UUID: uuid, + ProfileID: profileID, + } + 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 36b4df1..506e112 100644 --- a/internal/api/dashboards/v1/http/delete_test.go +++ b/internal/api/dashboards/v1/http/delete_test.go @@ -1,10 +1,14 @@ 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" @@ -12,6 +16,8 @@ import ( ) func TestServeDelete(t *testing.T) { + userName := "unnamed" + var profileID int64 = 1 dashboardUUID := "064dc707-02b8-7000-8201-02a7f396738a" type mockArgs struct { @@ -22,48 +28,82 @@ func TestServeDelete(t *testing.T) { tests := []struct { name string - wantErr bool + uuid string + wantStatus int + mockArgs *mockArgs + noUser bool }{ { - name: "ok", + name: "success", + uuid: dashboardUUID, + wantStatus: http.StatusOK, mockArgs: &mockArgs{ req: types.DeleteDashboardRequest{ - UUID: dashboardUUID, + UUID: dashboardUUID, + ProfileID: profileID, }, }, }, { - name: "err_svc", - wantErr: true, + 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, mockArgs: &mockArgs{ req: types.DeleteDashboardRequest{ - UUID: dashboardUUID, + UUID: dashboardUUID, + ProfileID: profileID, }, - err: errSomethingWrong, + 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"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + 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)) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - DeleteDashboard(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.err). - Times(1) + 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) } - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveDelete, + WantStatus: tt.wantStatus, }) }) } diff --git a/internal/api/dashboards/v1/http/get_all.go b/internal/api/dashboards/v1/http/get_all.go index 49e3023..6380216 100644 --- a/internal/api/dashboards/v1/http/get_all.go +++ b/internal/api/dashboards/v1/http/get_all.go @@ -44,11 +44,16 @@ 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 94c3b6a..f50fdd6 100644 --- a/internal/api/dashboards/v1/http/get_all_test.go +++ b/internal/api/dashboards/v1/http/get_all_test.go @@ -1,7 +1,12 @@ package http import ( + "context" + "errors" + "fmt" "net/http" + "net/http/httptest" + "strings" "testing" "go.uber.org/mock/gomock" @@ -11,6 +16,15 @@ 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 @@ -20,25 +34,22 @@ func TestServeGetAll(t *testing.T) { tests := []struct { name string - req getAllRequest - want getAllResponse - wantErr bool + reqBody string + wantRespBody string + wantStatus int mockArgs *mockArgs + noUser bool }{ { - 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"}, - }, - }, + 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, mockArgs: &mockArgs{ req: types.GetAllDashboardsRequest{ - Limit: testLimit, - Offset: testOffset, + Limit: limit, + Offset: offset, }, resp: types.DashboardInfosWithOwner{ { @@ -59,39 +70,62 @@ func TestServeGetAll(t *testing.T) { }, }, { - name: "err_svc", - req: getAllRequest{Limit: testLimit, Offset: testOffset}, - wantErr: true, + 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, mockArgs: &mockArgs{ req: types.GetAllDashboardsRequest{ - Limit: testLimit, - Offset: testOffset, + Limit: limit, + Offset: offset, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + req := httptest.NewRequest(http.MethodPost, "/dashboards/v1/all", strings.NewReader(tt.reqBody)) + api, mockedRepo := newTestData(t) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - GetAllDashboards(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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) } - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveGetAll, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } diff --git a/internal/api/dashboards/v1/http/get_by_uuid.go b/internal/api/dashboards/v1/http/get_by_uuid.go index d6a8e23..09bad62 100644 --- a/internal/api/dashboards/v1/http/get_by_uuid.go +++ b/internal/api/dashboards/v1/http/get_by_uuid.go @@ -32,6 +32,12 @@ 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 f3844fe..e17b483 100644 --- a/internal/api/dashboards/v1/http/get_by_uuid_test.go +++ b/internal/api/dashboards/v1/http/get_by_uuid_test.go @@ -1,10 +1,14 @@ 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" @@ -12,6 +16,8 @@ import ( ) func TestServeGetByUUID(t *testing.T) { + userName := "unnamed" + var profileID int64 = 1 dashboardUUID := "064dc707-02b8-7000-8201-02a7f396738a" type mockArgs struct { @@ -23,18 +29,18 @@ func TestServeGetByUUID(t *testing.T) { tests := []struct { name string - want dashboard - wantErr bool + uuid string + wantRespBody string + wantStatus int mockArgs *mockArgs + noUser bool }{ { - name: "ok", - want: dashboard{ - Name: "dashboard1", - Meta: "meta1", - OwnerName: "owner", - }, + name: "success", + uuid: dashboardUUID, + wantRespBody: `{"name":"dashboard1","meta":"meta1","owner_name":"owner"}`, + wantStatus: http.StatusOK, mockArgs: &mockArgs{ uuid: dashboardUUID, resp: types.Dashboard{ @@ -45,34 +51,59 @@ func TestServeGetByUUID(t *testing.T) { }, }, { - name: "err_svc", - wantErr: true, + 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, mockArgs: &mockArgs{ uuid: dashboardUUID, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + 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)) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - GetDashboardByUUID(gomock.Any(), tt.mockArgs.uuid). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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) } - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveGetByUUID, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } diff --git a/internal/api/dashboards/v1/http/get_my.go b/internal/api/dashboards/v1/http/get_my.go index 661e386..66c182f 100644 --- a/internal/api/dashboards/v1/http/get_my.go +++ b/internal/api/dashboards/v1/http/get_my.go @@ -44,11 +44,17 @@ func (a *API) serveGetMy(w http.ResponseWriter, r *http.Request) { }, ) - req := types.GetUserDashboardsRequest{ - Limit: httpReq.Limit, - Offset: httpReq.Offset, + 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, + } 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 e3a0a44..6965487 100644 --- a/internal/api/dashboards/v1/http/get_my_test.go +++ b/internal/api/dashboards/v1/http/get_my_test.go @@ -1,7 +1,12 @@ package http import ( + "context" + "errors" + "fmt" "net/http" + "net/http/httptest" + "strings" "testing" "go.uber.org/mock/gomock" @@ -11,6 +16,15 @@ 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 @@ -20,25 +34,23 @@ func TestServeGetMy(t *testing.T) { tests := []struct { name string - req getMyRequest - want getMyResponse - wantErr bool + reqBody string + wantRespBody string + wantStatus int mockArgs *mockArgs + noUser bool }{ { - 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"}, - }, - }, + 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, mockArgs: &mockArgs{ req: types.GetUserDashboardsRequest{ - Limit: testLimit, - Offset: testOffset, + ProfileID: profileID, + Limit: limit, + Offset: offset, }, resp: types.DashboardInfos{ {UUID: "064dc707-02b8-7000-8201-02a7f396738a", Name: "dashboard1"}, @@ -47,39 +59,63 @@ func TestServeGetMy(t *testing.T) { }, }, { - name: "err_svc", - req: getMyRequest{Limit: testLimit, Offset: testOffset}, - wantErr: true, + 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, mockArgs: &mockArgs{ req: types.GetUserDashboardsRequest{ - Limit: testLimit, - Offset: testOffset, + ProfileID: profileID, + Limit: limit, + Offset: offset, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedRepo := setupTestAPI(t) + req := httptest.NewRequest(http.MethodPost, "/dashboards/v1/my", strings.NewReader(tt.reqBody)) + api, mockedRepo := newTestData(t) if tt.mockArgs != nil { - mockedRepo.EXPECT(). - GetMyDashboards(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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) } - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveGetMy, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } diff --git a/internal/api/dashboards/v1/http/search.go b/internal/api/dashboards/v1/http/search.go index 71e3728..a297b69 100644 --- a/internal/api/dashboards/v1/http/search.go +++ b/internal/api/dashboards/v1/http/search.go @@ -57,6 +57,12 @@ 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 d972192..a47becb 100644 --- a/internal/api/dashboards/v1/http/search_test.go +++ b/internal/api/dashboards/v1/http/search_test.go @@ -1,7 +1,12 @@ package http import ( + "context" + "errors" + "fmt" "net/http" + "net/http/httptest" + "strings" "testing" "go.uber.org/mock/gomock" @@ -12,7 +17,27 @@ import ( func TestServeSearch(t *testing.T) { userName := "unnamed" - query := "test-query" + 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() + } type mockArgs struct { req types.SearchDashboardsRequest @@ -23,26 +48,23 @@ func TestServeSearch(t *testing.T) { tests := []struct { name string - req searchRequest - want searchResponse - wantErr bool + reqBody string + wantRespBody string + wantStatus int mockArgs *mockArgs + noUser bool }{ { - 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"}, - }, - }, + 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, mockArgs: &mockArgs{ req: types.SearchDashboardsRequest{ Query: query, - Limit: testLimit, - Offset: testOffset, + Limit: limit, + Offset: offset, }, resp: types.DashboardInfosWithOwner{ { @@ -63,26 +85,16 @@ func TestServeSearch(t *testing.T) { }, }, { - 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}, - }, - }, + 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, mockArgs: &mockArgs{ req: types.SearchDashboardsRequest{ Query: query, - Limit: testLimit, - Offset: testOffset, - Filter: &types.SearchDashboardsFilter{ - OwnerName: &userName, - }, + Limit: limit, + Offset: offset, + Filter: filter, }, resp: types.DashboardInfosWithOwner{ { @@ -96,40 +108,63 @@ func TestServeSearch(t *testing.T) { }, }, { - name: "err_svc", - req: searchRequest{Query: query, Limit: testLimit, Offset: testOffset}, - wantErr: true, + 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, mockArgs: &mockArgs{ req: types.SearchDashboardsRequest{ Query: query, - Limit: testLimit, - Offset: testOffset, + Limit: limit, + Offset: offset, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + req := httptest.NewRequest(http.MethodPost, "/dashboards/v1/search", strings.NewReader(tt.reqBody)) + api, mockedRepo := newTestData(t) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - SearchDashboards(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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) } - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveSearch, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } diff --git a/internal/api/dashboards/v1/http/test_data.go b/internal/api/dashboards/v1/http/test_data.go index dbdc1f0..4c1df04 100644 --- a/internal/api/dashboards/v1/http/test_data.go +++ b/internal/api/dashboards/v1/http/test_data.go @@ -1,35 +1,13 @@ package http import ( - "context" - "errors" - "net/http" "testing" - "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 + "github.com/ozontech/seq-ui/internal/api/dashboards/v1/test" + repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository/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) - } +func newTestData(t *testing.T) (*API, *repo_mock.MockDashboards) { + mock, s, p := test.NewTestData(t) + return New(s, p), mock } diff --git a/internal/api/dashboards/v1/http/update.go b/internal/api/dashboards/v1/http/update.go index 5f23b4d..8979ba5 100644 --- a/internal/api/dashboards/v1/http/update.go +++ b/internal/api/dashboards/v1/http/update.go @@ -48,13 +48,19 @@ func (a *API) serveUpdate(w http.ResponseWriter, r *http.Request) { }, ) - req := types.UpdateDashboardRequest{ - UUID: uuid, - Name: httpReq.Name, - Meta: httpReq.Meta, + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + httputil.ProcessError(wr, err) + return } - err := a.service.UpdateDashboard(ctx, req) + req := types.UpdateDashboardRequest{ + UUID: uuid, + ProfileID: profileID, + Name: httpReq.Name, + Meta: httpReq.Meta, + } + 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 bd1226e..b9be7f4 100644 --- a/internal/api/dashboards/v1/http/update_test.go +++ b/internal/api/dashboards/v1/http/update_test.go @@ -1,10 +1,15 @@ 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" @@ -12,11 +17,27 @@ import ( ) func TestServeUpdate(t *testing.T) { - var ( - dashboardUUID = "064dc707-02b8-7000-8201-02a7f396738a" - dashboardMeta = "my_meta" - dashboardName = "my_dashboard" - ) + 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() + } type mockArgs struct { req types.UpdateDashboardRequest @@ -26,55 +47,147 @@ func TestServeUpdate(t *testing.T) { tests := []struct { name string - req updateRequest - wantErr bool + uuid string + reqBody string + wantStatus int mockArgs *mockArgs + noUser bool }{ { - name: "ok", - req: updateRequest{Name: &dashboardName, Meta: &dashboardMeta}, + 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, mockArgs: &mockArgs{ req: types.UpdateDashboardRequest{ - UUID: dashboardUUID, - Name: &dashboardName, - Meta: &dashboardMeta, + ProfileID: profileID, + UUID: dashboardUUID, + Name: &dashboardName, }, }, }, { - name: "err_svc", - req: updateRequest{Name: &dashboardName, Meta: &dashboardMeta}, - wantErr: true, + name: "success_only_meta", + uuid: dashboardUUID, + reqBody: formatReqBody("", dashboardMeta), + wantStatus: http.StatusOK, mockArgs: &mockArgs{ req: types.UpdateDashboardRequest{ - UUID: dashboardUUID, - Name: &dashboardName, - Meta: &dashboardMeta, + ProfileID: profileID, + UUID: dashboardUUID, + Meta: &dashboardMeta, }, - err: errSomethingWrong, + }, + }, + { + 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"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + 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)) if tt.mockArgs != nil { - mockedSvc.EXPECT().UpdateDashboard(gomock.Any(), tt.mockArgs.req). + 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)) + api.profiles.SetID(userName, profileID) + } - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveUpdate, + WantStatus: tt.wantStatus, }) }) } diff --git a/internal/api/dashboards/v1/test/data.go b/internal/api/dashboards/v1/test/data.go new file mode 100644 index 0000000..5ae9e80 --- /dev/null +++ b/internal/api/dashboards/v1/test/data.go @@ -0,0 +1,23 @@ +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 bb0b2c5..fc424cf 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" - api "github.com/ozontech/seq-ui/pkg/massexport/v1" + massexport_v1 "github.com/ozontech/seq-ui/pkg/massexport/v1" ) type API struct { - api.UnimplementedMassExportServiceServer + massexport_v1.UnimplementedMassExportServiceServer exporter massexport.Service } diff --git a/internal/pkg/service/profiles/profiles.go b/internal/api/profiles/profiles.go similarity index 58% rename from internal/pkg/service/profiles/profiles.go rename to internal/api/profiles/profiles.go index 829bf4a..616a966 100644 --- a/internal/pkg/service/profiles/profiles.go +++ b/internal/api/profiles/profiles.go @@ -5,43 +5,30 @@ import ( "sync" "github.com/ozontech/seq-ui/internal/app/types" + "github.com/ozontech/seq-ui/internal/pkg/service" ) -type userProfileService interface { - GetOrCreateUserProfile(context.Context, types.GetOrCreateUserProfileRequest) (types.UserProfile, error) -} - -var profile *profiles - -func InitProfiles(svc userProfileService) { - profile = &profiles{ - idByName: make(map[string]int64), - service: svc, - } -} - -type profiles struct { +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) + service service.Service } -func SetID(userName string, userProfileID int64) { - profile.setID(userName, userProfileID) +func New(svc service.Service) *Profiles { + return &Profiles{ + idByName: make(map[string]int64), + service: svc, + } } -func (p *profiles) getIDFromContext(ctx context.Context) (int64, error) { +func (p *Profiles) GeIDFromContext(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 } @@ -49,7 +36,7 @@ func (p *profiles) getIDFromContext(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() @@ -75,7 +62,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/api/seqapi/v1/grpc/aggregation_test.go b/internal/api/seqapi/v1/grpc/aggregation_test.go index 785a824..d2b40ef 100644 --- a/internal/api/seqapi/v1/grpc/aggregation_test.go +++ b/internal/api/seqapi/v1/grpc/aggregation_test.go @@ -18,14 +18,15 @@ import ( ) func TestGetAggregation(t *testing.T) { - var ( - query = "message:error" - ) + query := "message:error" + from := time.Now() + to := from.Add(time.Second) + tests := []struct { name string req *seqapi.GetAggregationRequest - want *seqapi.GetAggregationResponse + resp *seqapi.GetAggregationResponse apiErr bool clientErr error @@ -36,14 +37,14 @@ func TestGetAggregation(t *testing.T) { name: "ok_multi_agg", req: &seqapi.GetAggregationRequest{ Query: query, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + From: timestamppb.New(from), + To: timestamppb.New(to), Aggregations: []*seqapi.AggregationQuery{ {Field: "test1"}, {Field: "test2"}, }, }, - want: &seqapi.GetAggregationResponse{ + resp: &seqapi.GetAggregationResponse{ Aggregations: test.MakeAggregations(2, 3, nil), Error: &seqapi.Error{ Code: seqapi.ErrorCode_ERROR_CODE_NO, @@ -75,8 +76,8 @@ func TestGetAggregation(t *testing.T) { name: "err_client", req: &seqapi.GetAggregationRequest{ Query: query, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + From: timestamppb.New(from), + To: timestamppb.New(to), Aggregations: []*seqapi.AggregationQuery{ {Field: "test2"}, }, @@ -89,8 +90,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() @@ -102,40 +103,39 @@ 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.want), tt.clientErr). - Times(1) + seqDbMock.EXPECT().GetAggregation(gomock.Any(), proto.Clone(tt.req)). + Return(proto.Clone(tt.resp), tt.clientErr).Times(1) seqData.Mocks.SeqDB = seqDbMock } - api := setupTestAPI(seqData) + s := initTestAPI(seqData) - resp, err := api.GetAggregation(context.Background(), tt.req) + resp, err := s.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.want)) + require.True(t, proto.Equal(resp, tt.resp)) }) } } func TestGetAggregationWithNormalization(t *testing.T) { - var ( - query = "message:error" - interval = "2s" - targetBucketRate = "3s" - ) + query := "message:error" + from := time.Now() + to := from.Add(time.Second) + targetBucketRate := "3s" + interval := "2s" + tests := []struct { name string - req *seqapi.GetAggregationRequest - want *seqapi.GetAggregationResponse - wantNormalized *seqapi.GetAggregationResponse + req *seqapi.GetAggregationRequest + resp *seqapi.GetAggregationResponse + normalized_resp *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(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + From: timestamppb.New(from), + To: timestamppb.New(to), Aggregations: []*seqapi.AggregationQuery{ {Field: "test1", Func: seqapi.AggFunc_AGG_FUNC_COUNT, Interval: &interval}, {Field: "test2", Func: seqapi.AggFunc_AGG_FUNC_COUNT, Interval: &interval}, }, }, - want: &seqapi.GetAggregationResponse{ + resp: &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, }, }, - wantNormalized: &seqapi.GetAggregationResponse{ + normalized_resp: &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(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + From: timestamppb.New(from), + To: timestamppb.New(to), 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}, }, }, - want: &seqapi.GetAggregationResponse{ + resp: &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, }, }, - wantNormalized: &seqapi.GetAggregationResponse{ + normalized_resp: &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.want), tt.clientErr).Times(1) + Return(proto.Clone(tt.resp), tt.clientErr).Times(1) seqData.Mocks.SeqDB = seqDbMock } - api := setupTestAPI(seqData) + s := initTestAPI(seqData) - resp, err := api.GetAggregation(context.Background(), tt.req) + resp, err := s.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.wantNormalized)) + require.True(t, proto.Equal(resp, tt.normalized_resp)) }) } } diff --git a/internal/api/seqapi/v1/grpc/api.go b/internal/api/seqapi/v1/grpc/api.go index da9a784..44d66a1 100644 --- a/internal/api/seqapi/v1/grpc/api.go +++ b/internal/api/seqapi/v1/grpc/api.go @@ -10,6 +10,7 @@ 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" @@ -38,7 +39,8 @@ type API struct { inmemWithRedisCache cache.Cache redisCache cache.Cache nowFn func() time.Time - asyncSearches asyncsearches.Service + asyncSearches *asyncsearches.Service + profiles *profiles.Profiles envsResponse *seqapi.GetEnvsResponse } @@ -47,7 +49,8 @@ func New( seqDBСlients map[string]seqdb.Client, inmemWithRedisCache cache.Cache, redisCache cache.Cache, - asyncSearches asyncsearches.Service, + asyncSearches *asyncsearches.Service, + p *profiles.Profiles, ) *API { var globalfCache *fieldsCache if cfg.FieldsCacheTTL > 0 { @@ -118,6 +121,7 @@ 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 2506f36..e54f926 100644 --- a/internal/api/seqapi/v1/grpc/cancel_async_search.go +++ b/internal/api/seqapi/v1/grpc/cancel_async_search.go @@ -14,7 +14,10 @@ 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()) } @@ -33,7 +36,12 @@ func (a *API) CancelAsyncSearch(ctx context.Context, req *seqapi.CancelAsyncSear }, ) - resp, err := a.asyncSearches.CancelAsyncSearch(ctx, req) + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + return nil, grpcutil.ProcessError(err) + } + + resp, err := a.asyncSearches.CancelAsyncSearch(ctx, profileID, 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 293fb1b..610cd92 100644 --- a/internal/api/seqapi/v1/grpc/cancel_async_search_test.go +++ b/internal/api/seqapi/v1/grpc/cancel_async_search_test.go @@ -8,98 +8,152 @@ 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_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" + mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" + mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) -func TestCancelAsyncSearch(t *testing.T) { +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 { - req *seqapi.CancelAsyncSearchRequest - resp *seqapi.CancelAsyncSearchResponse - err error + userName string + + profilesReq *types.GetOrCreateUserProfileRequest + profilesResp *types.UserProfile + profilesErr error + + repoResp *types.AsyncSearchInfo + repoErr error } tests := []struct { name string - req *seqapi.CancelAsyncSearchRequest - want *seqapi.CancelAsyncSearchResponse - wantCode codes.Code + req *seqapi.CancelAsyncSearchRequest + resp *seqapi.CancelAsyncSearchResponse + err error mockArgs *mockArgs }{ { name: "ok", req: &seqapi.CancelAsyncSearchRequest{ - SearchId: testSearchID, + SearchId: mockSearchID1, }, - want: &seqapi.CancelAsyncSearchResponse{}, + resp: &seqapi.CancelAsyncSearchResponse{}, mockArgs: &mockArgs{ - req: &seqapi.CancelAsyncSearchRequest{ - SearchId: testSearchID, + userName: mockUserName1, + profilesReq: &types.GetOrCreateUserProfileRequest{ + UserName: mockUserName1, + }, + profilesResp: &types.UserProfile{ + ID: mockProfileID1, + UserName: mockUserName1, + }, + repoResp: &types.AsyncSearchInfo{ + SearchID: mockSearchID1, + OwnerID: mockProfileID1, + OwnerName: mockUserName1, }, - resp: &seqapi.CancelAsyncSearchResponse{}, }, }, { - name: "invalid_id", + name: "err_permission_denied", req: &seqapi.CancelAsyncSearchRequest{ - SearchId: "some_invalid_id", + SearchId: mockSearchID1, }, - wantCode: codes.InvalidArgument, + 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, + }, + }, + err: status.Error(codes.PermissionDenied, "permission denied: cancel async search"), }, { - name: "err_svc", + name: "invalid id", req: &seqapi.CancelAsyncSearchRequest{ - SearchId: testSearchID, + SearchId: "some_invalid_id", }, - wantCode: codes.Internal, mockArgs: &mockArgs{ - req: &seqapi.CancelAsyncSearchRequest{ - SearchId: testSearchID, - }, - err: errSomethingWrong, + userName: mockUserName1, }, + 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 { - svcMock.EXPECT(). - CancelAsyncSearch(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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 + } } - api := setupTestAPI(seqData) - got, err := api.CancelAsyncSearch(context.Background(), tt.req) + api := initTestAPIWithAsyncSearches(seqData) + + ctx := context.Background() + ctx = context.WithValue(ctx, types.UserKey{}, tt.mockArgs.userName) - require.Equal(t, tt.wantCode, status.Code(err)) - if tt.wantCode != codes.OK { - return + 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.want, got) }) } } -func TestCancelAsyncSearch_Disabled(t *testing.T) { +func TestServeCancelAsyncSearch_Disabled(t *testing.T) { seqData := test.APITestData{} - api := setupTestAPI(seqData) + api := initTestAPI(seqData) - _, err := api.CancelAsyncSearch(context.Background(), &seqapi.CancelAsyncSearchRequest{SearchId: testSearchID}) + _, err := api.CancelAsyncSearch(context.Background(), &seqapi.CancelAsyncSearchRequest{}) 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 e7bb0b2..8dfb5c3 100644 --- a/internal/api/seqapi/v1/grpc/cluster_status_test.go +++ b/internal/api/seqapi/v1/grpc/cluster_status_test.go @@ -2,112 +2,82 @@ 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 mockArgs struct { - resp *seqapi.StatusResponse - err error + type TestCase struct { + name string + resp *seqapi.StatusResponse + clientErr error } - tests := []struct { - name string + someMoment := time.Now() - want *seqapi.StatusResponse - wantCode codes.Code - - mockArgs *mockArgs - }{ + tests := []TestCase{ { name: "ok", - want: &seqapi.StatusResponse{ + resp: &seqapi.StatusResponse{ NumberOfStores: 3, - OldestStorageTime: timestamppb.New(testTimestamp), + OldestStorageTime: timestamppb.New(someMoment), Stores: []*seqapi.StoreStatus{ { Host: "host-0", - Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(testTimestamp)}, + Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(someMoment)}, }, { Host: "host-1", - Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(testTimestamp.Add(1 * time.Hour))}, + Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(someMoment.Add(1 * time.Hour))}, }, { Host: "host-2", - 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))}, - }, + Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(someMoment.Add(2 * time.Hour))}, }, }, }, }, { - name: "err_client", - wantCode: codes.Internal, - mockArgs: &mockArgs{ - err: status.Error(codes.Internal, "client error"), - }, + name: "err_client", + 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().Status(gomock.Any(), nil). + Return(proto.Clone(tt.resp), tt.clientErr).Times(1) - seqDbMock.EXPECT(). - Status(gomock.Any(), nil). - Return(proto.Clone(tt.mockArgs.resp), tt.mockArgs.err). - Times(1) + cfg := config.SeqAPI{} seqData := test.APITestData{ + Cfg: cfg, Mocks: test.Mocks{ SeqDB: seqDbMock, }, } + s := initTestAPI(seqData) + resp, err := s.Status(context.Background(), nil) - 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)) + require.Equal(t, tt.clientErr, err) + require.True(t, proto.Equal(tt.resp, resp)) }) } } diff --git a/internal/api/seqapi/v1/grpc/delete_async_search.go b/internal/api/seqapi/v1/grpc/delete_async_search.go index 4c573ea..98b3928 100644 --- a/internal/api/seqapi/v1/grpc/delete_async_search.go +++ b/internal/api/seqapi/v1/grpc/delete_async_search.go @@ -14,7 +14,10 @@ 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()) } @@ -33,7 +36,12 @@ func (a *API) DeleteAsyncSearch(ctx context.Context, req *seqapi.DeleteAsyncSear }, ) - resp, err := a.asyncSearches.DeleteAsyncSearch(ctx, req) + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + return nil, grpcutil.ProcessError(err) + } + + resp, err := a.asyncSearches.DeleteAsyncSearch(ctx, profileID, 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 70942eb..f785ad4 100644 --- a/internal/api/seqapi/v1/grpc/delete_async_search_test.go +++ b/internal/api/seqapi/v1/grpc/delete_async_search_test.go @@ -8,98 +8,163 @@ 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_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" + mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" + mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) -func TestDeleteAsyncSearch(t *testing.T) { +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 { - req *seqapi.DeleteAsyncSearchRequest - resp *seqapi.DeleteAsyncSearchResponse - err error + userName string + + profilesReq *types.GetOrCreateUserProfileRequest + profilesResp *types.UserProfile + profilesErr error + + repoGetAsyncSearchResp *types.AsyncSearchInfo + repoGetAsyncSearchErr error + + repoDeleteAsyncSearchErr error } tests := []struct { name string - req *seqapi.DeleteAsyncSearchRequest - want *seqapi.DeleteAsyncSearchResponse - wantCode codes.Code + req *seqapi.DeleteAsyncSearchRequest + resp *seqapi.DeleteAsyncSearchResponse + err error + + shouldDelete bool mockArgs *mockArgs }{ { name: "ok", req: &seqapi.DeleteAsyncSearchRequest{ - SearchId: testSearchID, + SearchId: mockSearchID1, }, - want: &seqapi.DeleteAsyncSearchResponse{}, + resp: &seqapi.DeleteAsyncSearchResponse{}, mockArgs: &mockArgs{ - req: &seqapi.DeleteAsyncSearchRequest{ - SearchId: testSearchID, + userName: mockUserName1, + profilesReq: &types.GetOrCreateUserProfileRequest{ + UserName: mockUserName1, + }, + profilesResp: &types.UserProfile{ + ID: mockProfileID1, + UserName: mockUserName1, + }, + repoGetAsyncSearchResp: &types.AsyncSearchInfo{ + SearchID: mockSearchID1, + OwnerID: mockProfileID1, + OwnerName: mockUserName1, }, - resp: &seqapi.DeleteAsyncSearchResponse{}, }, + shouldDelete: true, }, { - name: "invalid_id", + name: "err_permission_denied", req: &seqapi.DeleteAsyncSearchRequest{ - SearchId: "some_invalid_id", + SearchId: mockSearchID1, }, - wantCode: codes.InvalidArgument, + 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, + }, + }, + err: status.Error(codes.PermissionDenied, "permission denied: delete async search"), }, { - name: "err_svc", + name: "invalid id", req: &seqapi.DeleteAsyncSearchRequest{ - SearchId: testSearchID, + SearchId: "some_invalid_id", }, - wantCode: codes.Internal, mockArgs: &mockArgs{ - req: &seqapi.DeleteAsyncSearchRequest{ - SearchId: testSearchID, - }, - err: errSomethingWrong, + userName: mockUserName1, }, + 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 { - svcMock.EXPECT(). - DeleteAsyncSearch(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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 + } } - api := setupTestAPI(seqData) - got, err := api.DeleteAsyncSearch(context.Background(), tt.req) + api := initTestAPIWithAsyncSearches(seqData) + + ctx := context.Background() + ctx = context.WithValue(ctx, types.UserKey{}, tt.mockArgs.userName) - require.Equal(t, tt.wantCode, status.Code(err)) - if tt.wantCode != codes.OK { - return + 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.want, got) }) } } func TestServeDeleteAsyncSearch_Disabled(t *testing.T) { seqData := test.APITestData{} - api := setupTestAPI(seqData) + api := initTestAPI(seqData) - _, err := api.DeleteAsyncSearch(context.Background(), &seqapi.DeleteAsyncSearchRequest{SearchId: testSearchID}) + _, err := api.DeleteAsyncSearch(context.Background(), &seqapi.DeleteAsyncSearchRequest{}) 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 3f10c1f..29b116e 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) { - var ( - id1 = "test1" - id2 = "test2" - id3 = "test3" - id4 = "test4" - ) - event1 := test.MakeEvent(id1, 1, testTimestamp) + 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) event1json, _ := proto.Marshal(event1) - event2 := test.MakeEvent(id2, 2, testTimestamp) + event2 := test.MakeEvent(id2, 2, eventTime) 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: errSomethingWrong, + Err: err, }, }, { @@ -82,7 +82,7 @@ func TestGetEvent(t *testing.T) { cacheArgs: test.CacheMockArgs{ Key: id3, Value: string(event3json), - Err: errSomethingWrong, + Err: err, }, }, { @@ -92,13 +92,13 @@ func TestGetEvent(t *testing.T) { }, cacheArgs: test.CacheMockArgs{ Key: id4, - Err: errSomethingWrong, + Err: err, }, clientErr: errors.New("client error"), }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -112,29 +112,23 @@ 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 := setupTestAPI(seqData) + s := initTestAPI(seqData) md := metadata.New(map[string]string{"env": "test"}) ctx := metadata.NewIncomingContext(context.Background(), md) @@ -152,21 +146,22 @@ 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 }{ @@ -339,7 +334,7 @@ func TestGetEventWithMasking(t *testing.T) { Data: map[string]string{ eventField: eventVal, }, - Time: timestamppb.New(testTimestamp), + Time: timestamppb.New(eventTime), } if shouldMask { event.Data[eventField] = "***" @@ -356,11 +351,13 @@ func TestGetEventWithMasking(t *testing.T) { } eventsData := make([]eventData, 0, len(tests)) - for i := range len(tests) { + for i := 0; i < len(tests); i++ { 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() @@ -375,20 +372,18 @@ func TestGetEventWithMasking(t *testing.T) { }, }, } - ctrl := gomock.NewController(t) + cacheMock := mock_cache.NewMockCache(ctrl) cacheArgs := test.CacheMockArgs{ Key: curEID, Value: string(curEData.eventJson), } if !tt.isCached { - cacheArgs.Err = errCache + cacheArgs.Err = cacheErr } - 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 { @@ -397,23 +392,25 @@ 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) } - api := setupTestAPI(seqData) + s := initTestAPI(seqData) + req := &seqapi.GetEventRequest{Id: curEID} - resp, err := api.GetEvent(context.Background(), req) - require.NoError(t, err) + resp, err := s.GetEvent(context.Background(), req) + + require.Equal(t, tt.wantErr, err) + if tt.wantErr != nil { + return + } + 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 3ba0fd4..1d3d9fa 100644 --- a/internal/api/seqapi/v1/grpc/fetch_async_search_result.go +++ b/internal/api/seqapi/v1/grpc/fetch_async_search_result.go @@ -14,7 +14,10 @@ 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 fe6ad7d..48fdb67 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,48 +9,49 @@ 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_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" + mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" + mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) func TestServeFetchAsyncSearchResult(t *testing.T) { var ( - meta = `{"some":"meta"}` + mockSearchID = "c9a34cf8-4c66-484e-9cc2-42979d848656" + mockTime = time.Date(2025, 8, 6, 17, 52, 12, 123, time.UTC) + meta = `{"some":"meta"}` ) - type mockArgs struct { - req *seqapi.FetchAsyncSearchResultRequest - err error - } tests := []struct { name string - req *seqapi.FetchAsyncSearchResultRequest - want *seqapi.FetchAsyncSearchResultResponse - wantCode codes.Code + req *seqapi.FetchAsyncSearchResultRequest + resp *seqapi.FetchAsyncSearchResultResponse + + repoResp types.AsyncSearchInfo - mockArgs *mockArgs + err error }{ { name: "ok", req: &seqapi.FetchAsyncSearchResultRequest{ - SearchId: testSearchID, + SearchId: mockSearchID, Limit: 2, Offset: 10, Order: seqapi.Order_ORDER_DESC, }, - want: &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(testTimestamp.Add(-15 * time.Minute)), - To: timestamppb.New(testTimestamp), + From: timestamppb.New(mockTime.Add(-15 * time.Minute)), + To: timestamppb.New(mockTime), Aggs: []*seqapi.AggregationQuery{ { Field: "x", @@ -74,7 +75,7 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { "message": "some error", "x": "2", }, - Time: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), + Time: timestamppb.New(mockTime.Add(-1 * time.Minute)), }, { Id: "017a854298010000-8502fe7f2aa33df3", @@ -83,7 +84,7 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { "message": "some error 2", "x": "8", }, - Time: timestamppb.New(testTimestamp.Add(-2 * time.Minute)), + Time: timestamppb.New(mockTime.Add(-2 * time.Minute)), }, }, Total: 2, @@ -107,14 +108,14 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Value: pointerTo(2), NotExists: 0, Quantiles: []float64{2, 1}, - Ts: timestamppb.New(testTimestamp), + Ts: timestamppb.New(mockTime), }, { Key: "2", Value: pointerTo(8), NotExists: 1, Quantiles: []float64{7, 4}, - Ts: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), + Ts: timestamppb.New(mockTime.Add(-1 * time.Minute)), }, }, NotExists: 2, @@ -125,36 +126,32 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Message: "some error", }, }, - StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, Meta: meta, }, - mockArgs: &mockArgs{ - req: &seqapi.FetchAsyncSearchResultRequest{ - SearchId: testSearchID, - Limit: 2, - Offset: 10, - Order: seqapi.Order_ORDER_DESC, - }, + repoResp: types.AsyncSearchInfo{ + SearchID: mockSearchID, + Meta: meta, }, }, { name: "partial_response", req: &seqapi.FetchAsyncSearchResultRequest{ - SearchId: testSearchID, + SearchId: mockSearchID, Limit: 2, Offset: 10, Order: seqapi.Order_ORDER_DESC, }, - want: &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(testTimestamp.Add(-15 * time.Minute)), - To: timestamppb.New(testTimestamp), + From: timestamppb.New(mockTime.Add(-15 * time.Minute)), + To: timestamppb.New(mockTime), WithDocs: true, Size: 100, }, @@ -167,7 +164,7 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { "message": "some error", "x": "2", }, - Time: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), + Time: timestamppb.New(mockTime.Add(-1 * time.Minute)), }, }, Total: 1, @@ -175,8 +172,8 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Code: seqapi.ErrorCode_ERROR_CODE_NO, }, }, - StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, Meta: meta, @@ -185,77 +182,60 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Message: "partial response", }, }, - mockArgs: &mockArgs{ - req: &seqapi.FetchAsyncSearchResultRequest{ - SearchId: testSearchID, - Limit: 2, - Offset: 10, - Order: seqapi.Order_ORDER_DESC, - }, + repoResp: types.AsyncSearchInfo{ + SearchID: mockSearchID, + Meta: meta, }, }, { - name: "invalid_id", + name: "invalid id", req: &seqapi.FetchAsyncSearchResultRequest{ SearchId: "some_invalid_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, - }, + err: status.Error(codes.InvalidArgument, "invalid search_id"), }, } - 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) - seqData := test.APITestData{} - seqData.Mocks.AsyncSearchesSvc = svcMock + 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 - if tt.mockArgs != nil { - svcMock.EXPECT(). - FetchAsyncSearchResult(gomock.Any(), tt.mockArgs.req). - Return(tt.want, tt.mockArgs.err). - Times(1) + seqDbMock := mock_seqdb.NewMockClient(ctrl) + seqDbMock.EXPECT().FetchAsyncSearchResult(gomock.Any(), tt.req). + Return(tt.resp, nil).Times(1) + seqData.Mocks.SeqDB = seqDbMock } - api := setupTestAPI(seqData) - got, err := api.FetchAsyncSearchResult(context.Background(), tt.req) + api := initTestAPIWithAsyncSearches(seqData) + + ctx := context.Background() - require.Equal(t, tt.wantCode, status.Code(err)) - if tt.wantCode != codes.OK { - return + 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.want, got) }) } } func TestServeFetchAsyncSearchResult_Disabled(t *testing.T) { seqData := test.APITestData{} - api := setupTestAPI(seqData) + api := initTestAPI(seqData) - _, err := api.FetchAsyncSearchResult(context.Background(), &seqapi.FetchAsyncSearchResultRequest{SearchId: testSearchID}) + _, err := api.FetchAsyncSearchResult(context.Background(), &seqapi.FetchAsyncSearchResultRequest{}) 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 f52149d..3bdf539 100644 --- a/internal/api/seqapi/v1/grpc/fields_test.go +++ b/internal/api/seqapi/v1/grpc/fields_test.go @@ -103,18 +103,16 @@ 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 := mock_seqdb.NewMockClient(ctrl) + seqDbMock.EXPECT().GetFields(gomock.Any(), nil). + Return(proto.Clone(tt.seqDBResp), tt.clientErr).Times(1) seqData := test.APITestData{ Mocks: test.Mocks{ @@ -125,8 +123,11 @@ func TestGetFields(t *testing.T) { }, } - api := setupTestAPI(seqData) - resp, err := api.GetFields(context.Background(), nil) + s := initTestAPI(seqData) + + ctx := context.Background() + + resp, err := s.GetFields(ctx, nil) require.Equal(t, tt.clientErr, err) require.True(t, proto.Equal(tt.wantResp, resp)) @@ -135,9 +136,6 @@ func TestGetFields(t *testing.T) { } func TestGetFieldsCached(t *testing.T) { - var ( - ttl = 10 * time.Millisecond - ) responses := []*seqapi.GetFieldsResponse{ { Fields: []*seqapi.Field{ @@ -165,12 +163,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{ @@ -181,17 +179,16 @@ func TestGetFieldsCached(t *testing.T) { SeqDB: seqDbMock, }, } - - api := setupTestAPI(seqData) + s := initTestAPI(seqData) for _, r := range responses { - resp, err := api.GetFields(context.Background(), nil) + resp, err := s.GetFields(context.Background(), nil) require.NoError(t, err) require.True(t, proto.Equal(r, resp)) time.Sleep(ttl / 2) - resp, err = api.GetFields(context.Background(), nil) + resp, err = s.GetFields(context.Background(), nil) require.NoError(t, err) require.True(t, proto.Equal(r, resp)) @@ -215,8 +212,8 @@ func TestGetPinnedFields(t *testing.T) { name: "empty", }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -227,10 +224,9 @@ func TestGetPinnedFields(t *testing.T) { }, }, } + s := initTestAPI(seqData) - api := setupTestAPI(seqData) - - resp, err := api.GetPinnedFields(context.Background(), nil) + resp, err := s.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 b639207..f60cb9e 100644 --- a/internal/api/seqapi/v1/grpc/get_async_searches_list.go +++ b/internal/api/seqapi/v1/grpc/get_async_searches_list.go @@ -13,7 +13,10 @@ 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 88d4237..9b6c994 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,54 +9,65 @@ 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_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" + mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" + mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) func TestServeGetAsyncSearchesList(t *testing.T) { var ( - errorMsg = "some err" - mockUserName1 = "some_user_1" - mockUserName2 = "some_user_2" - mockSearchID2 = "9e4c068e-d4f4-4a5d-be27-a6524a70d70d" + 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) ) + type mockArgs struct { - req *seqapi.GetAsyncSearchesListRequest - err error + searchIDs []string + + repoReq types.GetAsyncSearchesListRequest + repoResp []types.AsyncSearchInfo + repoErr error } tests := []struct { name string - req *seqapi.GetAsyncSearchesListRequest - want *seqapi.GetAsyncSearchesListResponse - wantCode codes.Code + req *seqapi.GetAsyncSearchesListRequest + resp *seqapi.GetAsyncSearchesListResponse + err error mockArgs *mockArgs }{ { name: "ok_no_filters", req: &seqapi.GetAsyncSearchesListRequest{}, - want: &seqapi.GetAsyncSearchesListResponse{ + resp: &seqapi.GetAsyncSearchesListResponse{ Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchId: testSearchID, + SearchId: mockSearchID1, 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), + From: timestamppb.New(mockTime.Add(-15 * time.Minute)), + To: timestamppb.New(mockTime), WithDocs: true, Size: 100, }, - StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, OwnerName: mockUserName1, @@ -68,8 +79,8 @@ func TestServeGetAsyncSearchesList(t *testing.T) { 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), + From: timestamppb.New(mockTime.Add(-1 * time.Hour)), + To: timestamppb.New(mockTime), Aggs: []*seqapi.AggregationQuery{ { Field: "x", @@ -82,9 +93,9 @@ func TestServeGetAsyncSearchesList(t *testing.T) { }, WithDocs: false, }, - StartedAt: timestamppb.New(testTimestamp.Add(-60 * time.Second)), - ExpiresAt: timestamppb.New(testTimestamp.Add(300 * time.Second)), - CanceledAt: timestamppb.New(testTimestamp), + StartedAt: timestamppb.New(mockTime.Add(-60 * time.Second)), + ExpiresAt: timestamppb.New(mockTime.Add(300 * time.Second)), + CanceledAt: timestamppb.New(mockTime), Progress: 1, DiskUsage: 256, OwnerName: mockUserName2, @@ -92,7 +103,20 @@ func TestServeGetAsyncSearchesList(t *testing.T) { }, }, mockArgs: &mockArgs{ - req: &seqapi.GetAsyncSearchesListRequest{}, + repoReq: types.GetAsyncSearchesListRequest{}, + repoResp: []types.AsyncSearchInfo{ + { + SearchID: mockSearchID1, + OwnerID: mockProfileID1, + OwnerName: mockUserName1, + }, + { + SearchID: mockSearchID2, + OwnerID: mockProfileID2, + OwnerName: mockUserName2, + }, + }, + searchIDs: []string{mockSearchID1, mockSearchID2}, }, }, { @@ -103,21 +127,21 @@ func TestServeGetAsyncSearchesList(t *testing.T) { Limit: 10, Offset: 20, }, - want: &seqapi.GetAsyncSearchesListResponse{ + resp: &seqapi.GetAsyncSearchesListResponse{ Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchId: testSearchID, + SearchId: mockSearchID1, 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), + From: timestamppb.New(mockTime.Add(-15 * time.Minute)), + To: timestamppb.New(mockTime), WithDocs: true, Size: 100, }, - StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, OwnerName: mockUserName1, @@ -125,32 +149,37 @@ func TestServeGetAsyncSearchesList(t *testing.T) { }, }, mockArgs: &mockArgs{ - req: &seqapi.GetAsyncSearchesListRequest{ - Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE.Enum(), - OwnerName: &mockUserName1, - Limit: 10, - Offset: 20, + repoReq: types.GetAsyncSearchesListRequest{ + Owner: &mockUserName1, + }, + repoResp: []types.AsyncSearchInfo{ + { + SearchID: mockSearchID1, + OwnerID: mockProfileID1, + OwnerName: mockUserName1, + }, }, + searchIDs: []string{mockSearchID1}, }, }, { name: "partial_response", req: &seqapi.GetAsyncSearchesListRequest{}, - want: &seqapi.GetAsyncSearchesListResponse{ + resp: &seqapi.GetAsyncSearchesListResponse{ Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchId: testSearchID, + SearchId: mockSearchID1, 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), + From: timestamppb.New(mockTime.Add(-15 * time.Minute)), + To: timestamppb.New(mockTime), WithDocs: true, Size: 100, }, - StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, OwnerName: mockUserName1, @@ -162,7 +191,15 @@ func TestServeGetAsyncSearchesList(t *testing.T) { }, }, mockArgs: &mockArgs{ - req: &seqapi.GetAsyncSearchesListRequest{}, + repoReq: types.GetAsyncSearchesListRequest{}, + repoResp: []types.AsyncSearchInfo{ + { + SearchID: mockSearchID1, + OwnerID: mockProfileID1, + OwnerName: mockUserName1, + }, + }, + searchIDs: []string{mockSearchID1}, }, }, { @@ -171,7 +208,7 @@ func TestServeGetAsyncSearchesList(t *testing.T) { Limit: -10, Offset: 10, }, - wantCode: codes.InvalidArgument, + err: status.Error(codes.InvalidArgument, "invalid request field: 'limit' must be non-negative"), }, { name: "err_offset", @@ -179,42 +216,48 @@ func TestServeGetAsyncSearchesList(t *testing.T) { Limit: 10, Offset: -10, }, - wantCode: codes.InvalidArgument, + err: status.Error(codes.InvalidArgument, "invalid request field: 'offset' must be non-negative"), }, } - 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 { - svcMock.EXPECT(). - GetAsyncSearchesList(gomock.Any(), tt.mockArgs.req). - Return(tt.want, tt.mockArgs.err). - Times(1) + 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 } - api := setupTestAPI(seqData) - got, err := api.GetAsyncSearchesList(context.Background(), tt.req) + api := initTestAPIWithAsyncSearches(seqData) + + ctx := context.Background() - require.Equal(t, tt.wantCode, status.Code(err)) - if tt.wantCode != codes.OK { - return + 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.want, got) }) } } func TestServeGetAsyncSearchesList_Disabled(t *testing.T) { seqData := test.APITestData{} - api := setupTestAPI(seqData) + api := initTestAPI(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 5f5a1a4..7f8140e 100644 --- a/internal/api/seqapi/v1/grpc/get_envs_test.go +++ b/internal/api/seqapi/v1/grpc/get_envs_test.go @@ -141,6 +141,7 @@ 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 8b4d25b..aa75fda 100644 --- a/internal/api/seqapi/v1/grpc/histogram_test.go +++ b/internal/api/seqapi/v1/grpc/histogram_test.go @@ -2,6 +2,7 @@ package grpc import ( "context" + "errors" "testing" "time" @@ -16,15 +17,16 @@ import ( ) func TestGetHistogram(t *testing.T) { - var ( - query = "message:error" - interval = "2s" - ) + query := "message:error" + from := time.Now() + to := from.Add(time.Second) + interval := "5s" + tests := []struct { name string req *seqapi.GetHistogramRequest - want *seqapi.GetHistogramResponse + resp *seqapi.GetHistogramResponse clientErr error }{ @@ -32,11 +34,11 @@ func TestGetHistogram(t *testing.T) { name: "ok", req: &seqapi.GetHistogramRequest{ Query: query, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + From: timestamppb.New(from), + To: timestamppb.New(to), Interval: interval, }, - want: &seqapi.GetHistogramResponse{ + resp: &seqapi.GetHistogramResponse{ Histogram: test.MakeHistogram(5), Error: &seqapi.Error{ Code: seqapi.ErrorCode_ERROR_CODE_NO, @@ -48,11 +50,11 @@ func TestGetHistogram(t *testing.T) { req: &seqapi.GetHistogramRequest{ Interval: interval, }, - clientErr: errSomethingWrong, + clientErr: errors.New("client error"), }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -61,17 +63,16 @@ 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.want), tt.clientErr). - Times(1) + seqDbMock.EXPECT().GetHistogram(gomock.Any(), proto.Clone(tt.req)). + Return(proto.Clone(tt.resp), tt.clientErr).Times(1) + seqData.Mocks.SeqDB = seqDbMock + s := initTestAPI(seqData) - api := setupTestAPI(seqData) - resp, err := api.GetHistogram(context.Background(), tt.req) + resp, err := s.GetHistogram(context.Background(), tt.req) require.Equal(t, tt.clientErr, err) - require.True(t, proto.Equal(tt.want, resp)) + require.True(t, proto.Equal(tt.resp, resp)) }) } } diff --git a/internal/api/seqapi/v1/grpc/limits_test.go b/internal/api/seqapi/v1/grpc/limits_test.go index 30892c6..c361d79 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 := setupTestAPI(seqData) + s := initTestAPI(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 9b2fc2f..872f927 100644 --- a/internal/api/seqapi/v1/grpc/logs_lifespan_test.go +++ b/internal/api/seqapi/v1/grpc/logs_lifespan_test.go @@ -2,6 +2,7 @@ package grpc import ( "context" + "errors" "strconv" "testing" "time" @@ -21,17 +22,21 @@ import ( ) func TestGetLogsLifespan(t *testing.T) { - var ( - resultStr = "36000" // 10(h) * 60(min/h) * 60(sec/min) - cacheKey = "logs_lifespan" + const ( + cacheKey = "logs_lifespan" + cacheTTL = 1 * time.Minute + result = 10 * time.Hour - cacheTTL = time.Minute + resultStr = "36000" // 10(h) * 60(min/h) * 60(sec/min) ) + 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 @@ -61,7 +66,7 @@ func TestGetLogsLifespan(t *testing.T) { Value: resultStr, }, clientResp: &seqapi.StatusResponse{ - OldestStorageTime: timestamppb.New(testTimestamp), + OldestStorageTime: timestamppb.New(oldestStorageTime), }, resp: &seqapi.GetLogsLifespanResponse{ Lifespan: durationpb.New(result), @@ -76,7 +81,7 @@ func TestGetLogsLifespan(t *testing.T) { Value: resultStr, }, clientResp: &seqapi.StatusResponse{ - OldestStorageTime: timestamppb.New(testTimestamp), + OldestStorageTime: timestamppb.New(oldestStorageTime), }, resp: &seqapi.GetLogsLifespanResponse{ Lifespan: durationpb.New(result), @@ -87,7 +92,7 @@ func TestGetLogsLifespan(t *testing.T) { getOp: test.CacheMockArgs{ Err: cache.ErrNotFound, }, - clientErr: errSomethingWrong, + clientErr: errors.New("network error"), }, { name: "err_nil_oldest_storage_time", @@ -99,8 +104,8 @@ func TestGetLogsLifespan(t *testing.T) { }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -112,35 +117,28 @@ func TestGetLogsLifespan(t *testing.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 := mock_cache.NewMockCache(ctrl) + 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 := setupTestAPI(seqData) + s := initTestAPI(seqData) s.nowFn = func() time.Time { - return testTimestamp.Add(result) + return oldestStorageTime.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 b818531..d83d22a 100644 --- a/internal/api/seqapi/v1/grpc/search_test.go +++ b/internal/api/seqapi/v1/grpc/search_test.go @@ -2,6 +2,7 @@ package grpc import ( "context" + "errors" "testing" "time" @@ -18,10 +19,13 @@ import ( ) func TestSearch(t *testing.T) { - var ( - query = "message:error" - limit int32 = 3 - ) + 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) + tests := []struct { name string @@ -38,8 +42,8 @@ func TestSearch(t *testing.T) { name: "ok", req: &seqapi.SearchRequest{ Query: query, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + From: timestamppb.New(from), + To: timestamppb.New(to), Limit: limit, Offset: 0, WithTotal: true, @@ -53,7 +57,7 @@ func TestSearch(t *testing.T) { Order: seqapi.Order_ORDER_ASC, }, resp: &seqapi.SearchResponse{ - Events: test.MakeEvents(int(limit), testTimestamp), + Events: test.MakeEvents(int(limit), eventTime), Total: int64(limit), Histogram: test.MakeHistogram(2), Aggregations: test.MakeAggregations(2, 2, nil), @@ -109,8 +113,8 @@ func TestSearch(t *testing.T) { name: "err_offset_too_high", req: &seqapi.SearchRequest{ Query: query, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + From: timestamppb.New(from), + To: timestamppb.New(to), Limit: limit, Offset: 11, }, @@ -126,18 +130,18 @@ func TestSearch(t *testing.T) { name: "err_total_too_high", req: &seqapi.SearchRequest{ Query: query, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + From: timestamppb.New(from), + To: timestamppb.New(to), Limit: limit, Offset: 0, WithTotal: true, }, resp: &seqapi.SearchResponse{ - Events: test.MakeEvents(int(limit), testTimestamp), + Events: test.MakeEvents(int(limit), eventTime), Total: int64(limit) + 1, }, wantResp: &seqapi.SearchResponse{ - Events: test.MakeEvents(int(limit), testTimestamp), + Events: test.MakeEvents(int(limit), eventTime), Total: int64(limit) + 1, Error: &seqapi.Error{ Code: seqapi.ErrorCode_ERROR_CODE_QUERY_TOO_HEAVY, @@ -155,8 +159,8 @@ func TestSearch(t *testing.T) { name: "err_client", req: &seqapi.SearchRequest{ Query: query, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + From: timestamppb.New(from), + To: timestamppb.New(to), Limit: limit, Offset: 0, }, @@ -165,11 +169,11 @@ func TestSearch(t *testing.T) { MaxSearchLimit: 5, }, }), - clientErr: errSomethingWrong, + clientErr: errors.New("client error"), }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -181,16 +185,14 @@ 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 } - api := setupTestAPI(seqData) - resp, err := api.Search(context.Background(), tt.req) + s := initTestAPI(seqData) + resp, err := s.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 86641ed..4c31659 100644 --- a/internal/api/seqapi/v1/grpc/start_async_search.go +++ b/internal/api/seqapi/v1/grpc/start_async_search.go @@ -14,7 +14,10 @@ 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()) } @@ -64,7 +67,12 @@ func (a *API) StartAsyncSearch(ctx context.Context, req *seqapi.StartAsyncSearch span.SetAttributes(spanAttributes...) - resp, err := a.asyncSearches.StartAsyncSearch(ctx, req) + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + return nil, grpcutil.ProcessError(err) + } + + resp, err := a.asyncSearches.StartAsyncSearch(ctx, profileID, 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 03cbc21..14c0dd9 100644 --- a/internal/api/seqapi/v1/grpc/start_async_search_test.go +++ b/internal/api/seqapi/v1/grpc/start_async_search_test.go @@ -9,31 +9,43 @@ 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_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" + mock_seqdb "github.com/ozontech/seq-ui/internal/pkg/client/seqdb/mock" + mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) func TestServeStartAsyncSearch(t *testing.T) { - var ( - query = "message:error" - meta = `{"some":"meta"}` + const ( + mockSearchID = "c9a34cf8-4c66-484e-9cc2-42979d848656" + mockUserName = "some_user" + mockProfileID = 1 + 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 { - resp *seqapi.StartAsyncSearchResponse - err error + profilesReq types.GetOrCreateUserProfileRequest + profilesResp types.UserProfile + profilesErr error + + repoReq types.SaveAsyncSearchRequest + repoErr error } tests := []struct { name string - req *seqapi.StartAsyncSearchRequest - want *seqapi.StartAsyncSearchResponse - wantCode codes.Code + req *seqapi.StartAsyncSearchRequest + resp *seqapi.StartAsyncSearchResponse mockArgs *mockArgs }{ @@ -42,8 +54,8 @@ func TestServeStartAsyncSearch(t *testing.T) { req: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), Query: query, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + From: timestamppb.New(from), + To: timestamppb.New(to), WithDocs: true, Size: 100, Hist: &seqapi.StartAsyncSearchRequest_HistQuery{ @@ -59,44 +71,25 @@ func TestServeStartAsyncSearch(t *testing.T) { }, Meta: meta, }, - want: &seqapi.StartAsyncSearchResponse{ - SearchId: testSearchID, + resp: &seqapi.StartAsyncSearchResponse{ + SearchId: mockSearchID, }, mockArgs: &mockArgs{ - resp: &seqapi.StartAsyncSearchResponse{ - SearchId: testSearchID, + profilesReq: types.GetOrCreateUserProfileRequest{ + 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", + profilesResp: types.UserProfile{ + ID: mockProfileID, + UserName: mockUserName, }, - Aggs: []*seqapi.AggregationQuery{ - { - Field: "v", - GroupBy: "level", - Func: seqapi.AggFunc_AGG_FUNC_AVG, - Quantiles: []float64{0.95}, - }, + repoReq: types.SaveAsyncSearchRequest{ + SearchID: mockSearchID, + OwnerID: mockProfileID, + Meta: meta, }, - Meta: meta, - }, - wantCode: codes.Internal, - mockArgs: &mockArgs{ - err: errSomethingWrong, }, }, } - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -105,31 +98,39 @@ func TestServeStartAsyncSearch(t *testing.T) { if tt.mockArgs != nil { ctrl := gomock.NewController(t) - svcMock := mock_asyncsearches.NewMockService(ctrl) - svcMock.EXPECT(). - StartAsyncSearch(gomock.Any(), tt.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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 - seqData.Mocks.AsyncSearchesSvc = svcMock + asyncSearchesRepoMock := mock_repo.NewMockAsyncSearches(ctrl) + asyncSearchesRepoMock.EXPECT().SaveAsyncSearch(gomock.Any(), gomock.Any()). + Return(tt.mockArgs.repoErr).Times(1) + seqData.Mocks.AsyncSearchesRepo = asyncSearchesRepoMock } - api := setupTestAPI(seqData) - got, err := api.StartAsyncSearch(context.Background(), tt.req) + api := initTestAPIWithAsyncSearches(seqData) - require.Equal(t, tt.wantCode, status.Code(err)) - if tt.wantCode != codes.OK { - return - } - require.Equal(t, tt.want, got) + 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)) }) } } func TestServeStartAsyncSearch_Disabled(t *testing.T) { seqData := test.APITestData{} - api := setupTestAPI(seqData) + api := initTestAPI(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 a088897..4b605ea 100644 --- a/internal/api/seqapi/v1/grpc/test_data.go +++ b/internal/api/seqapi/v1/grpc/test_data.go @@ -1,23 +1,18 @@ package grpc import ( - "errors" - "time" + "context" + "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" ) -// 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 { +func initTestAPI(data test.APITestData) *API { // when test cases don't explicitly provide configuration if data.Cfg.SeqAPIOptions == nil { data.Cfg.SeqAPIOptions = &config.SeqAPIOptions{} @@ -29,10 +24,20 @@ func setupTestAPI(data test.APITestData) *API { seqDBClients[envConfig.SeqDB] = data.Mocks.SeqDB } - var asyncSvc asyncsearches.Service - if data.Mocks.AsyncSearchesSvc != nil { - asyncSvc = data.Mocks.AsyncSearchesSvc - } + return New(data.Cfg, seqDBClients, data.Mocks.Cache, data.Mocks.Cache, nil, nil) +} - 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, + } + 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/http/aggregation_test.go b/internal/api/seqapi/v1/http/aggregation_test.go index 6a675f4..d567c8a 100644 --- a/internal/api/seqapi/v1/http/aggregation_test.go +++ b/internal/api/seqapi/v1/http/aggregation_test.go @@ -1,11 +1,16 @@ 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" @@ -17,6 +22,21 @@ 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 @@ -26,26 +46,21 @@ func TestServeGetAggregation(t *testing.T) { tests := []struct { name string - req getAggregationRequest - want getAggregationResponse - wantErr bool + reqBody string + wantRespBody string + wantStatus int mockArgs *mockArgs cfg config.SeqAPI }{ { - name: "ok_single_agg", - req: getAggregationRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - AggField: "test_single", - }, + name: "ok_single_agg", + reqBody: formatReqBody("test_single", nil), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), AggField: "test_single", }, resp: &seqapi.GetAggregationResponse{ @@ -56,12 +71,8 @@ func TestServeGetAggregation(t *testing.T) { }, }, }, - want: getAggregationResponse{ - Aggregation: aggregationFromProto(test.MakeAggregation(2, nil)), - Aggregations: aggregationsFromProto(test.MakeAggregations(1, 2, nil), true), - Error: apiError{Code: aecNo}, - PartialResponse: false, - }, + 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, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxAggregationsPerRequest: 3, @@ -70,21 +81,16 @@ func TestServeGetAggregation(t *testing.T) { }, { name: "ok_multi_agg", - req: getAggregationRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - Aggregations: aggregationQueries{ - {Field: "test_multi1"}, - {Field: "test_multi2"}, - {Field: "test_multi3"}, - }, - }, + reqBody: formatReqBody("", aggregationQueries{ + {Field: "test_multi1"}, + {Field: "test_multi2"}, + {Field: "test_multi3"}, + }), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Aggregations: []*seqapi.AggregationQuery{ {Field: "test_multi1"}, {Field: "test_multi2"}, @@ -99,12 +105,8 @@ func TestServeGetAggregation(t *testing.T) { }, }, }, - want: getAggregationResponse{ - Aggregation: aggregationFromProto(test.MakeAggregation(3, nil)), - Aggregations: aggregationsFromProto(test.MakeAggregations(2, 3, nil), true), - Error: apiError{Code: aecNo}, - PartialResponse: false, - }, + 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, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxAggregationsPerRequest: 3, @@ -114,24 +116,19 @@ func TestServeGetAggregation(t *testing.T) { { name: "ok_agg_quantile", - 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}, - }, - }, - }, + reqBody: formatReqBody("", aggregationQueries{ + { + Field: "test_multi1", + GroupBy: "service", + Func: afQuantile, + Quantiles: []float64{0.95, 0.99}, + }, + }), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Aggregations: []*seqapi.AggregationQuery{ { Field: "test_multi1", @@ -155,18 +152,8 @@ func TestServeGetAggregation(t *testing.T) { }, }, }, - 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, - }, + 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, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxAggregationsPerRequest: 3, @@ -174,18 +161,13 @@ func TestServeGetAggregation(t *testing.T) { }, }, { - name: "err_partial_response", - req: getAggregationRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - AggField: "test_err_partial", - }, + name: "err_partial_response", + reqBody: formatReqBody("test_err_partial", nil), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), AggField: "test_err_partial", }, resp: &seqapi.GetAggregationResponse{ @@ -196,12 +178,8 @@ func TestServeGetAggregation(t *testing.T) { PartialResponse: true, }, }, - want: getAggregationResponse{ - Aggregation: aggregationFromProto(nil), - Aggregations: aggregationsFromProto(nil, true), - Error: apiError{Code: aecPartialResponse, Message: "partial response"}, - PartialResponse: true, - }, + wantRespBody: `{"aggregation":{"buckets":[]},"aggregations":[],"error":{"code":"ERROR_CODE_PARTIAL_RESPONSE","message":"partial response"},"partialResponse":true}`, + wantStatus: http.StatusOK, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxAggregationsPerRequest: 3, @@ -209,14 +187,14 @@ func TestServeGetAggregation(t *testing.T) { }, }, { - name: "err_aggs_limit_max", - req: getAggregationRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - Aggregations: aggregationQueries{{}, {}, {}}, - }, - wantErr: true, + name: "err_invalid_request", + reqBody: "invalid-request", + wantStatus: http.StatusBadRequest, + }, + { + name: "err_aggs_limit_max", + reqBody: formatReqBody("", aggregationQueries{{}, {}, {}}), + wantStatus: http.StatusBadRequest, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxAggregationsPerRequest: 2, @@ -224,23 +202,18 @@ func TestServeGetAggregation(t *testing.T) { }, }, { - name: "err_client", - req: getAggregationRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - AggField: "test_err_client", - }, + name: "err_client", + reqBody: formatReqBody("test_err_client", nil), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), AggField: "test_err_client", }, err: errors.New("client error"), }, - wantErr: true, + wantStatus: http.StatusInternalServerError, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxAggregationsPerRequest: 3, @@ -248,8 +221,8 @@ func TestServeGetAggregation(t *testing.T) { }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -259,25 +232,21 @@ 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 := mock_seqdb.NewMockClient(ctrl) + seqDbMock.EXPECT().GetAggregation(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) seqData.Mocks.SeqDB = seqDbMock } - 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, + 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, }) }) } diff --git a/internal/api/seqapi/v1/http/aggregation_ts_test.go b/internal/api/seqapi/v1/http/aggregation_ts_test.go index 9c4f86c..a7b20fa 100644 --- a/internal/api/seqapi/v1/http/aggregation_ts_test.go +++ b/internal/api/seqapi/v1/http/aggregation_ts_test.go @@ -23,6 +23,9 @@ 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" @@ -31,7 +34,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}`, - testQuery, testTimestamp.Format(time.RFC3339), testTimestamp.Add(time.Second).Format(time.RFC3339), aggQueriesRaw) + query, from.Format(time.RFC3339), to.Format(time.RFC3339), aggQueriesRaw) } type mockArgs struct { @@ -70,9 +73,9 @@ func TestServeGetAggregationTs(t *testing.T) { }), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), 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}, @@ -81,9 +84,9 @@ func TestServeGetAggregationTs(t *testing.T) { resp: &seqapi.GetAggregationResponse{ Aggregation: test.MakeAggregation(3, &test.MakeAggOpts{ Ts: []*timestamppb.Timestamp{ - timestamppb.New(testTimestamp.Add(time.Second)), - timestamppb.New(testTimestamp.Add(2 * time.Second)), - timestamppb.New(testTimestamp.Add(3 * time.Second)), + timestamppb.New(from.Add(time.Second)), + timestamppb.New(from.Add(2 * time.Second)), + timestamppb.New(from.Add(3 * time.Second)), }, Values: []float64{ 1, @@ -93,9 +96,9 @@ func TestServeGetAggregationTs(t *testing.T) { }), Aggregations: test.MakeAggregations(2, 3, &test.MakeAggOpts{ Ts: []*timestamppb.Timestamp{ - timestamppb.New(testTimestamp.Add(time.Second)), - timestamppb.New(testTimestamp.Add(2 * time.Second)), - timestamppb.New(testTimestamp.Add(3 * time.Second)), + timestamppb.New(from.Add(time.Second)), + timestamppb.New(from.Add(2 * time.Second)), + timestamppb.New(from.Add(3 * time.Second)), }, }), Error: &seqapi.Error{ @@ -133,9 +136,9 @@ func TestServeGetAggregationTs(t *testing.T) { }), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), 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}, @@ -144,9 +147,9 @@ func TestServeGetAggregationTs(t *testing.T) { resp: &seqapi.GetAggregationResponse{ Aggregations: test.MakeAggregations(2, 3, &test.MakeAggOpts{ Ts: []*timestamppb.Timestamp{ - timestamppb.New(testTimestamp.Add(time.Second)), - timestamppb.New(testTimestamp.Add(2 * time.Second)), - timestamppb.New(testTimestamp.Add(3 * time.Second)), + timestamppb.New(from.Add(time.Second)), + timestamppb.New(from.Add(2 * time.Second)), + timestamppb.New(from.Add(3 * time.Second)), }, Values: []float64{ 3, @@ -183,9 +186,9 @@ func TestServeGetAggregationTs(t *testing.T) { }), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Aggregations: []*seqapi.AggregationQuery{ { Field: "test_quantile1", @@ -200,17 +203,17 @@ func TestServeGetAggregationTs(t *testing.T) { Aggregation: test.MakeAggregation(3, &test.MakeAggOpts{ Quantiles: []float64{100, 150}, Ts: []*timestamppb.Timestamp{ - timestamppb.New(testTimestamp.Add(time.Second)), - timestamppb.New(testTimestamp.Add(2 * time.Second)), - timestamppb.New(testTimestamp.Add(3 * time.Second)), + timestamppb.New(from.Add(time.Second)), + timestamppb.New(from.Add(2 * time.Second)), + timestamppb.New(from.Add(3 * time.Second)), }, }), Aggregations: test.MakeAggregations(1, 3, &test.MakeAggOpts{ Quantiles: []float64{100, 150}, Ts: []*timestamppb.Timestamp{ - timestamppb.New(testTimestamp.Add(time.Second)), - timestamppb.New(testTimestamp.Add(2 * time.Second)), - timestamppb.New(testTimestamp.Add(3 * time.Second)), + timestamppb.New(from.Add(time.Second)), + timestamppb.New(from.Add(2 * time.Second)), + timestamppb.New(from.Add(3 * time.Second)), }, }), Error: &seqapi.Error{ @@ -232,9 +235,9 @@ func TestServeGetAggregationTs(t *testing.T) { reqBody: formatReqBody(nil), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), }, resp: &seqapi.GetAggregationResponse{ Error: &seqapi.Error{ @@ -280,7 +283,7 @@ func TestServeGetAggregationTs(t *testing.T) { cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxAggregationsPerRequest: 3, - MaxBucketsPerAggregationTs: 1, + MaxBucketsPerAggregationTs: 8, }, }, }, @@ -289,9 +292,9 @@ func TestServeGetAggregationTs(t *testing.T) { reqBody: formatReqBody(nil), mockArgs: &mockArgs{ req: &seqapi.GetAggregationRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), }, err: errors.New("client error"), }, @@ -321,7 +324,7 @@ func TestServeGetAggregationTs(t *testing.T) { seqData.Mocks.SeqDB = seqDbMock } - api := setupTestAPI(seqData) + api := initTestAPI(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 c762883..1dda605 100644 --- a/internal/api/seqapi/v1/http/api.go +++ b/internal/api/seqapi/v1/http/api.go @@ -10,6 +10,7 @@ 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" @@ -38,7 +39,8 @@ type API struct { inmemWithRedisCache cache.Cache redisCache cache.Cache nowFn func() time.Time - asyncSearches asyncsearches.Service + asyncSearches *asyncsearches.Service + profiles *profiles.Profiles envsResponse getEnvsResponse } @@ -47,7 +49,8 @@ func New( seqDBСlients map[string]seqdb.Client, inmemWithRedisCache cache.Cache, redisCache cache.Cache, - asyncSearches asyncsearches.Service, + asyncSearches *asyncsearches.Service, + p *profiles.Profiles, ) *API { var globalfCache *fieldsCache if cfg.FieldsCacheTTL > 0 { @@ -132,6 +135,7 @@ 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 b07776d..2c8c4f8 100644 --- a/internal/api/seqapi/v1/http/cancel_async_search.go +++ b/internal/api/seqapi/v1/http/cancel_async_search.go @@ -47,7 +47,15 @@ func (a *API) serveCancelAsyncSearch(w http.ResponseWriter, r *http.Request) { }, ) - _, err := a.asyncSearches.CancelAsyncSearch(ctx, &seqapi.CancelAsyncSearchRequest{SearchId: searchID}) + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + httputil.ProcessError(wr, err) + return + } + + _, err = a.asyncSearches.CancelAsyncSearch(ctx, profileID, &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 e904a2b..053df8b 100644 --- a/internal/api/seqapi/v1/http/cancel_async_search_test.go +++ b/internal/api/seqapi/v1/http/cancel_async_search_test.go @@ -1,63 +1,115 @@ 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" - mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" + "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" "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 { - req *seqapi.CancelAsyncSearchRequest - resp *seqapi.CancelAsyncSearchResponse - err error + userName string + + proxyReq *seqapi.CancelAsyncSearchRequest + proxyResp *seqapi.CancelAsyncSearchResponse + proxyErr error + + profilesReq *types.GetOrCreateUserProfileRequest + profilesResp *types.UserProfile + profilesErr error + + repoResp *types.AsyncSearchInfo + repoErr error } tests := []struct { name string - searchID string - wantErr bool - noResp bool + reqBody string + wantRespBody string + wantStatus int mockArgs *mockArgs }{ { - name: "ok", - searchID: testSearchID, - noResp: true, + name: "ok", mockArgs: &mockArgs{ - req: &seqapi.CancelAsyncSearchRequest{ - SearchId: testSearchID, + 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, }, - resp: &seqapi.CancelAsyncSearchResponse{}, }, + wantRespBody: ``, + wantStatus: http.StatusOK, }, { - name: "invalid_id", - searchID: "some invalid id", - wantErr: true, + 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: "err_svc", - searchID: testSearchID, - wantErr: true, + name: "invalid id", mockArgs: &mockArgs{ - req: &seqapi.CancelAsyncSearchRequest{ - SearchId: testSearchID, + userName: mockUserName1, + proxyReq: &seqapi.CancelAsyncSearchRequest{ + SearchId: "some_invalid_id", }, - 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() @@ -66,26 +118,45 @@ func TestServeCancelAsyncSearch(t *testing.T) { if tt.mockArgs != nil { ctrl := gomock.NewController(t) - svcMock := mock_asyncsearches.NewMockService(ctrl) - 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.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 } - seqData.Mocks.AsyncSearchesSvc = svcMock - } + 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 + } - api := setupTestAPI(seqData) + 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 + } + } - 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, + 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, }) }) } @@ -93,12 +164,17 @@ func TestServeCancelAsyncSearch(t *testing.T) { func TestServeCancelAsyncSearch_Disabled(t *testing.T) { seqData := test.APITestData{} - 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, + 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, }) } diff --git a/internal/api/seqapi/v1/http/cluster_status_test.go b/internal/api/seqapi/v1/http/cluster_status_test.go index 0151beb..448345e 100644 --- a/internal/api/seqapi/v1/http/cluster_status_test.go +++ b/internal/api/seqapi/v1/http/cluster_status_test.go @@ -1,8 +1,11 @@ package http import ( + "errors" "net/http" + "net/http/httptest" "testing" + "time" "go.uber.org/mock/gomock" "google.golang.org/protobuf/types/known/timestamppb" @@ -19,70 +22,64 @@ func TestStatus(t *testing.T) { err error } - tests := []struct { + type testCase struct { name string - want statusResponse - wantErr bool + wantRespBody string + wantStatus int - mockArgs *mockArgs - }{ + mockArgs mockArgs + } + + someMoment := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + + tests := []testCase{ { name: "ok", - want: statusResponse{ - OldestStorageTime: &testTimestamp, - NumberOfStores: 1, - Stores: []storeStatus{ - { - Host: "host-0", - Values: &storeStatusValues{OldestTime: &testTimestamp}, - }, - }, - }, - mockArgs: &mockArgs{ + mockArgs: mockArgs{ resp: &seqapi.StatusResponse{ NumberOfStores: 1, - OldestStorageTime: timestamppb.New(testTimestamp), + OldestStorageTime: timestamppb.New(someMoment), Stores: []*seqapi.StoreStatus{ { Host: "host-0", - Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(testTimestamp)}, + Values: &seqapi.StoreStatusValues{OldestTime: timestamppb.New(someMoment)}, }, }, }, }, + 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", - wantErr: true, - mockArgs: &mockArgs{ - err: errSomethingWrong, + name: "err_client", + mockArgs: mockArgs{ + err: errors.New("client error"), }, + wantStatus: http.StatusInternalServerError, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - - 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 := test.APITestData{} + 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 := setupTestAPI(seqData) - httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, statusResponse]{ - Method: http.MethodGet, - Target: "/seqapi/v1/status", - Handler: api.serveStatus, - Want: tt.want, - WantErr: tt.wantErr, + api := initTestAPI(seqData) + req := httptest.NewRequest(http.MethodGet, "/seqapi/v1/status", http.NoBody) + + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveStatus, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } diff --git a/internal/api/seqapi/v1/http/delete_async_search.go b/internal/api/seqapi/v1/http/delete_async_search.go index 28e64a7..addf2b9 100644 --- a/internal/api/seqapi/v1/http/delete_async_search.go +++ b/internal/api/seqapi/v1/http/delete_async_search.go @@ -47,7 +47,15 @@ func (a *API) serveDeleteAsyncSearch(w http.ResponseWriter, r *http.Request) { }, ) - _, err := a.asyncSearches.DeleteAsyncSearch(ctx, &seqapi.DeleteAsyncSearchRequest{SearchId: searchID}) + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + httputil.ProcessError(wr, err) + return + } + + _, err = a.asyncSearches.DeleteAsyncSearch(ctx, profileID, &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 a3802db..878adf6 100644 --- a/internal/api/seqapi/v1/http/delete_async_search_test.go +++ b/internal/api/seqapi/v1/http/delete_async_search_test.go @@ -1,63 +1,119 @@ 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" - mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" + "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" "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 { - req *seqapi.DeleteAsyncSearchRequest - resp *seqapi.DeleteAsyncSearchResponse - err error + 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 } tests := []struct { name string - searchID string - wantErr bool - noResp bool + reqBody string + wantRespBody string + wantStatus int + shouldDelete bool mockArgs *mockArgs }{ { - name: "ok", - searchID: testSearchID, - noResp: true, + name: "ok", mockArgs: &mockArgs{ - req: &seqapi.DeleteAsyncSearchRequest{ - SearchId: testSearchID, + 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, }, - resp: &seqapi.DeleteAsyncSearchResponse{}, }, + shouldDelete: true, + wantRespBody: ``, + wantStatus: http.StatusOK, }, { - name: "invalid_id", - searchID: "some invalid id", - wantErr: true, + 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: "err_svc", - searchID: testSearchID, - wantErr: true, + name: "invalid id", mockArgs: &mockArgs{ - req: &seqapi.DeleteAsyncSearchRequest{ - SearchId: testSearchID, + userName: mockUserName1, + proxyReq: &seqapi.DeleteAsyncSearchRequest{ + SearchId: "some_invalid_id", }, - 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() @@ -67,23 +123,50 @@ func TestServeDeleteAsyncSearch(t *testing.T) { if tt.mockArgs != nil { ctrl := gomock.NewController(t) - 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 + 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 + } } - 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, + 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, }) }) } @@ -91,12 +174,17 @@ func TestServeDeleteAsyncSearch(t *testing.T) { func TestServeDeleteAsyncSearch_Disabled(t *testing.T) { seqData := test.APITestData{} - 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, + 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, }) } diff --git a/internal/api/seqapi/v1/http/events_test.go b/internal/api/seqapi/v1/http/events_test.go index 7b33966..5646dbf 100644 --- a/internal/api/seqapi/v1/http/events_test.go +++ b/internal/api/seqapi/v1/http/events_test.go @@ -1,12 +1,16 @@ 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" @@ -21,47 +25,45 @@ import ( ) func TestServeGetEvent(t *testing.T) { - var ( - id1 = "test1" - id2 = "test2" - id3 = "test3" - id4 = "test4" - cacheTTL = time.Minute - ) - - event1 := test.MakeEvent(id1, 1, testTimestamp) - event1json, _ := proto.Marshal(event1) - event2 := test.MakeEvent(id2, 2, testTimestamp) - event2json, _ := proto.Marshal(event2) - event3 := test.MakeEvent(id3, 0, testTimestamp) - event3json, _ := proto.Marshal(event3) - - type mockArgs struct { + type seqDBArgs struct { req *seqapi.GetEventRequest resp *seqapi.GetEventResponse err error } + 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) + event1json, _ := proto.Marshal(event1) + event2 := test.MakeEvent(id2, 2, eventTime) + event2json, _ := proto.Marshal(event2) + event3 := test.MakeEvent(id3, 0, eventTime) + event3json, _ := proto.Marshal(event3) + err := errors.New("test error") + cacheTTL := time.Minute + tests := []struct { name string - id string - want getEventResponse - wantErr bool + id string + wantRespBody string + wantStatus int cacheArgs test.CacheMockArgs - mockArgs *mockArgs + seqDBArgs *seqDBArgs }{ { name: "ok_no_cached", id: id1, - want: getEventResponse{Event: eventFromProto(event1)}, cacheArgs: test.CacheMockArgs{ Key: id1, Value: string(event1json), - Err: errSomethingWrong, + Err: err, }, - mockArgs: &mockArgs{ + seqDBArgs: &seqDBArgs{ req: &seqapi.GetEventRequest{ Id: id1, }, @@ -69,26 +71,28 @@ 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: errSomethingWrong, + Err: err, }, - mockArgs: &mockArgs{ + seqDBArgs: &seqDBArgs{ req: &seqapi.GetEventRequest{ Id: id3, }, @@ -96,29 +100,31 @@ 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, - wantErr: true, + name: "err_client", + id: id4, cacheArgs: test.CacheMockArgs{ Key: id4, - Err: errSomethingWrong, + Err: err, }, - mockArgs: &mockArgs{ + seqDBArgs: &seqDBArgs{ 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) + seqData := test.APITestData{ Cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ @@ -128,57 +134,55 @@ 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.mockArgs != nil { + if tt.seqDBArgs != nil { seqDbMock := mock_seqdb.NewMockClient(ctrl) - seqDbMock.EXPECT(). - GetEvent(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + seqDbMock.EXPECT().GetEvent(gomock.Any(), tt.seqDBArgs.req). + Return(tt.seqDBArgs.resp, tt.seqDBArgs.err).Times(1) seqData.Mocks.SeqDB = seqDbMock - if tt.mockArgs.err == nil { - cacheMock.EXPECT(). - SetWithTTL(gomock.Any(), tt.cacheArgs.Key, tt.cacheArgs.Value, cacheTTL). - Return(nil). - Times(1) + if tt.seqDBArgs.err == nil { + cacheMock.EXPECT().SetWithTTL(gomock.Any(), tt.cacheArgs.Key, tt.cacheArgs.Value, cacheTTL). + Return(nil).Times(1) } } - api := setupTestAPI(seqData) + 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)) - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveGetEvent, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } } func TestGetEventWithMasking(t *testing.T) { - var ( - errCache = errors.New("test error") - cacheTTL = time.Minute - ) - type mockArgs struct { + type seqDBArgs 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 - wantErr bool + wantStatus int maskingCfg *config.Masking }{ @@ -186,6 +190,7 @@ func TestGetEventWithMasking(t *testing.T) { name: "mask_noncached", shouldMask: true, isCached: false, + wantStatus: http.StatusOK, maskingCfg: &config.Masking{ Masks: []config.Mask{ { @@ -211,6 +216,7 @@ func TestGetEventWithMasking(t *testing.T) { name: "mask_from_cache", shouldMask: true, isCached: true, + wantStatus: http.StatusOK, maskingCfg: &config.Masking{ Masks: []config.Mask{ { @@ -236,6 +242,7 @@ func TestGetEventWithMasking(t *testing.T) { name: "do_not_mask_noncached_regex", shouldMask: false, isCached: false, + wantStatus: http.StatusOK, maskingCfg: &config.Masking{ Masks: []config.Mask{ { @@ -261,6 +268,7 @@ 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{ { @@ -286,6 +294,7 @@ 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{ { @@ -311,6 +320,7 @@ 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{ { @@ -338,7 +348,7 @@ func TestGetEventWithMasking(t *testing.T) { id string event *seqapi.Event eventJson []byte - want getEventResponse + wantResp []byte } formEventData := func(i int, shouldMask bool) eventData { @@ -351,18 +361,20 @@ func TestGetEventWithMasking(t *testing.T) { Data: map[string]string{ eventField: eventVal, }, - Time: timestamppb.New(testTimestamp), + Time: timestamppb.New(eventTime), } 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, - want: getEventResponse{Event: eventFromProto(event)}, + wantResp: wantResp, } } @@ -372,10 +384,12 @@ 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) + curEData := eventsData[i] curEID := curEData.id @@ -394,46 +408,40 @@ func TestGetEventWithMasking(t *testing.T) { Value: string(curEData.eventJson), } if !tt.isCached { - cacheArgs.Err = errCache + cacheArgs.Err = cacheErr } - 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 { - mockArgs := &mockArgs{ + seqDBArgs := &seqDBArgs{ req: &seqapi.GetEventRequest{Id: curEData.id}, resp: &seqapi.GetEventResponse{Event: curEData.event}, } - seqDbMock.EXPECT(). - GetEvent(gomock.Any(), mockArgs.req). - Return(mockArgs.resp, nil). - Times(1) + seqDbMock.EXPECT().GetEvent(gomock.Any(), seqDBArgs.req). + Return(seqDBArgs.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 := setupTestAPI(seqData) + 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)) - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveGetEvent, + WantRespBody: string(curEData.wantResp), + WantStatus: tt.wantStatus, }) }) } diff --git a/internal/api/seqapi/v1/http/export_test.go b/internal/api/seqapi/v1/http/export_test.go index d203aa3..0b339fc 100644 --- a/internal/api/seqapi/v1/http/export_test.go +++ b/internal/api/seqapi/v1/http/export_test.go @@ -1,10 +1,17 @@ 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" @@ -16,6 +23,28 @@ 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 @@ -24,147 +53,115 @@ func TestServeExport(t *testing.T) { tests := []struct { name string - req exportRequest - cfg config.SeqAPI - wantErr bool + reqBody string + wantStatus int mockArgs *mockArgs + cfg config.SeqAPI }{ { - 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, - }, - }, + name: "ok_jsonl", + reqBody: formatReqBody(50, "", nil), mockArgs: &mockArgs{ req: &seqapi.ExportRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Limit: 50, Offset: 0, }, }, - }, - { - name: "ok_csv", - req: exportRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - Limit: 50, - Offset: 0, - Format: efCSV, - Fields: []string{"field1", "field2"}, - }, + wantStatus: http.StatusOK, 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: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), 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_parallel_limited", - req: exportRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - Limit: 0, - Offset: 0, - }, + name: "err_invalid_request", + reqBody: "invalid-request", + wantStatus: http.StatusBadRequest, + }, + { + name: "err_parallel_limited", + reqBody: formatReqBody(0, "", nil), + wantStatus: http.StatusTooManyRequests, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxParallelExportRequests: 0, }, }, - wantErr: true, }, { - name: "err_export_limit_max", - req: exportRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - Limit: 10, - Offset: 0, - }, + name: "err_export_limit_max", + reqBody: formatReqBody(10, "", nil), + wantStatus: http.StatusBadRequest, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxExportLimit: 5, MaxParallelExportRequests: 1, }, }, - wantErr: true, }, { - name: "err_csv_empty_fields", - req: exportRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - Limit: 10, - Offset: 0, - Format: efCSV, - }, + name: "err_csv_empty_fields", + reqBody: formatReqBody(10, efCSV, nil), + wantStatus: http.StatusBadRequest, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxExportLimit: 100, MaxParallelExportRequests: 1, }, }, - wantErr: true, }, { - name: "err_client", - req: exportRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - Limit: 50, - Offset: 0, + 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"), }, + 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() @@ -172,28 +169,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) - seqDbMock.EXPECT(). - Export(gomock.Any(), tt.mockArgs.req, gomock.Any()). - Return(tt.mockArgs.err). - Times(1) + cw, _ := httputil.NewChunkedWriter(w) + seqDbMock.EXPECT().Export(gomock.Any(), tt.mockArgs.req, cw). + Return(tt.mockArgs.err).Times(1) seqData.Mocks.SeqDB = seqDbMock } - api := setupTestAPI(seqData) + s := initTestAPI(seqData) + + s.serveExport(w, req) + + res := w.Result() + defer res.Body.Close() - 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, - }) + require.Equal(t, tt.wantStatus, res.StatusCode) }) } } 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 95c1175..c884917 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,6 +2,8 @@ package http import ( "net/http" + "net/http/httptest" + "strings" "testing" "time" @@ -11,163 +13,54 @@ import ( "github.com/ozontech/seq-ui/internal/api/httputil" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" - mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" + "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" "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 { - req *seqapi.FetchAsyncSearchResultRequest - resp *seqapi.FetchAsyncSearchResultResponse - err error + proxyReq *seqapi.FetchAsyncSearchResultRequest + proxyResp *seqapi.FetchAsyncSearchResultResponse + proxyErr error + + repoResp types.AsyncSearchInfo + repoErr error } tests := []struct { name string - req fetchAsyncSearchResultRequest - want fetchAsyncSearchResultResponse - wantErr bool + reqBody string + wantRespBody string + wantStatus int mockArgs *mockArgs }{ { - 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, - }, - }), + name: "ok", + reqBody: `{"search_id":"c9a34cf8-4c66-484e-9cc2-42979d848656","limit":2,"offset":10,"order":"desc"}`, mockArgs: &mockArgs{ - req: &seqapi.FetchAsyncSearchResultRequest{ - SearchId: testSearchID, + proxyReq: &seqapi.FetchAsyncSearchResultRequest{ + SearchId: mockSearchID, Limit: 2, Offset: 10, Order: seqapi.Order_ORDER_DESC, }, - resp: &seqapi.FetchAsyncSearchResultResponse{ + proxyResp: &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), + From: timestamppb.New(mockTime.Add(-15 * time.Minute)), + To: timestamppb.New(mockTime), Aggs: []*seqapi.AggregationQuery{ { Field: "x", @@ -197,7 +90,7 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { "message": "some error", "x": "2", }, - Time: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), + Time: timestamppb.New(mockTime.Add(-1 * time.Minute)), }, { Id: "017a854298010000-8502fe7f2aa33df3", @@ -206,7 +99,7 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { "message": "some error 2", "x": "8", }, - Time: timestamppb.New(testTimestamp.Add(-2 * time.Minute)), + Time: timestamppb.New(mockTime.Add(-2 * time.Minute)), }, }, Total: 2, @@ -245,19 +138,19 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Key: "33", Value: pointerTo[float64](2), NotExists: 0, - Ts: timestamppb.New(testTimestamp.Add(-30 * time.Second)), + Ts: timestamppb.New(mockTime.Add(-30 * time.Second)), }, { Key: "33", Value: pointerTo[float64](5), NotExists: 0, - Ts: timestamppb.New(testTimestamp), + Ts: timestamppb.New(mockTime), }, { Key: "22", Value: pointerTo[float64](8), NotExists: 1, - Ts: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), + Ts: timestamppb.New(mockTime.Add(-1 * time.Minute)), }, }, NotExists: 2, @@ -268,84 +161,39 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Message: "some error", }, }, - StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(mockTime.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", - 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", - }, - }), + name: "partial_response", + reqBody: `{"search_id":"c9a34cf8-4c66-484e-9cc2-42979d848656","limit":2,"offset":10,"order":"desc"}`, mockArgs: &mockArgs{ - req: &seqapi.FetchAsyncSearchResultRequest{ - SearchId: testSearchID, + proxyReq: &seqapi.FetchAsyncSearchResultRequest{ + SearchId: mockSearchID, Limit: 2, Offset: 10, Order: seqapi.Order_ORDER_DESC, }, - resp: &seqapi.FetchAsyncSearchResultResponse{ + proxyResp: &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), + From: timestamppb.New(mockTime.Add(-15 * time.Minute)), + To: timestamppb.New(mockTime), WithDocs: true, Size: 100, }, @@ -358,7 +206,7 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { "message": "some error", "x": "2", }, - Time: timestamppb.New(testTimestamp.Add(-1 * time.Minute)), + Time: timestamppb.New(mockTime.Add(-1 * time.Minute)), }, { Id: "017a854298010000-8502fe7f2aa33df3", @@ -367,7 +215,7 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { "message": "some error 2", "x": "8", }, - Time: timestamppb.New(testTimestamp.Add(-2 * time.Minute)), + Time: timestamppb.New(mockTime.Add(-2 * time.Minute)), }, }, Total: 2, @@ -376,8 +224,8 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { Message: "some error", }, }, - StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, Error: &seqapi.Error{ @@ -385,61 +233,66 @@ 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", - req: fetchAsyncSearchResultRequest{ - SearchID: testSearchID, - Limit: -10, - Offset: 20, - }, - wantErr: true, + 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", - req: fetchAsyncSearchResultRequest{ - SearchID: testSearchID, - Limit: 10, - Offset: -20, - }, - wantErr: true, + 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: "invalid_id", - req: fetchAsyncSearchResultRequest{ - SearchID: "some invalid id", - }, - wantErr: true, + name: "invalid id", + reqBody: `{"search_id":"some_invalid_id"}`, + wantRespBody: `{"message":"invalid request field: invalid uuid"}`, + wantStatus: http.StatusBadRequest, + }, + { + name: "err_invalid_request", + reqBody: "invalid-request", + wantStatus: http.StatusBadRequest, }, } - 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 { - svcMock.EXPECT(). - FetchAsyncSearchResult(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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 } - api := setupTestAPI(seqData) + api := initTestAPIWithAsyncSearches(seqData) + req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/async_search/fetch", strings.NewReader(tt.reqBody)) - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveFetchAsyncSearchResult, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } @@ -447,13 +300,14 @@ func TestServeFetchAsyncSearchResult(t *testing.T) { func TestServeFetchAsyncSearchResult_Disabled(t *testing.T) { seqData := test.APITestData{} - api := setupTestAPI(seqData) + api := initTestAPI(seqData) + req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/async_search/fetch", strings.NewReader("{}")) - httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[fetchAsyncSearchResultRequest, struct{}]{ - Method: http.MethodPost, - Target: "/seqapi/v1/async_search/fetch", - Handler: api.serveFetchAsyncSearchResult, - WantErr: true, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveFetchAsyncSearchResult, + WantRespBody: `{"message":"async searches disabled"}`, + WantStatus: http.StatusBadRequest, }) } diff --git a/internal/api/seqapi/v1/http/fields_test.go b/internal/api/seqapi/v1/http/fields_test.go index 81bfb7b..a22b61f 100644 --- a/internal/api/seqapi/v1/http/fields_test.go +++ b/internal/api/seqapi/v1/http/fields_test.go @@ -1,10 +1,16 @@ 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" @@ -25,20 +31,14 @@ func TestServeGetFields(t *testing.T) { cfg config.SeqAPIOptions - want getFieldsResponse - wantErr bool + wantRespBody string + wantStatus int - mockArgs *mockArgs + mockArgs mockArgs }{ { name: "ok", - want: getFieldsResponse{ - Fields: fields{ - {Name: "test_name1", Type: "keyword"}, - {Name: "test_name2", Type: "text"}, - }, - }, - mockArgs: &mockArgs{ + mockArgs: mockArgs{ resp: &seqapi.GetFieldsResponse{ Fields: []*seqapi.Field{ { @@ -52,24 +52,12 @@ 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", - 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{ + mockArgs: mockArgs{ resp: &seqapi.GetFieldsResponse{ Fields: []*seqapi.Field{ { @@ -93,17 +81,19 @@ 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", - wantErr: true, - mockArgs: &mockArgs{ - err: errSomethingWrong, + name: "err_client", + mockArgs: mockArgs{ + 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) @@ -115,38 +105,31 @@ 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 := setupTestAPI(seqData) + api := initTestAPI(seqData) + req := httptest.NewRequest(http.MethodGet, "/seqapi/v1/fields", http.NoBody) - httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getFieldsResponse]{ - Method: http.MethodGet, - Target: "/seqapi/v1/fields", - Handler: api.serveGetFields, - Want: tt.want, - WantErr: tt.wantErr, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveGetFields, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } } func TestServeGetFieldsCached(t *testing.T) { - var ( - ttl = 5 * time.Millisecond - ) - - tests := []struct { - name string + type TestCase struct { + resp *seqapi.GetFieldsResponse + wantRespBody string + } - resp *seqapi.GetFieldsResponse - want getFieldsResponse - }{ + tests := []TestCase{ { - name: "ok", resp: &seqapi.GetFieldsResponse{ Fields: []*seqapi.Field{ { @@ -159,15 +142,9 @@ func TestServeGetFieldsCached(t *testing.T) { }, }, }, - want: getFieldsResponse{ - Fields: fields{ - {Name: "n1", Type: "keyword"}, - {Name: "n2", Type: "text"}, - }, - }, + wantRespBody: `{"fields":[{"name":"n1","type":"keyword"},{"name":"n2","type":"text"}]}`, }, { - name: "another_ok", resp: &seqapi.GetFieldsResponse{ Fields: []*seqapi.Field{ { @@ -176,55 +153,51 @@ func TestServeGetFieldsCached(t *testing.T) { }, }, }, - want: getFieldsResponse{ - Fields: fields{ - {Name: "qwe", Type: "keyword"}, - }, - }, + wantRespBody: `{"fields":[{"name":"qwe","type":"keyword"}]}`, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() + ctrl := gomock.NewController(t) + seqDbMock := mock_seqdb.NewMockClient(ctrl) - ctrl := gomock.NewController(t) - seqDbMock := mock_seqdb.NewMockClient(ctrl) + for _, testCase := range tests { + seqDbMock.EXPECT().GetFields(gomock.Any(), gomock.Any()). + Return(testCase.resp, nil).Times(1) + } - seqDbMock.EXPECT(). - GetFields(gomock.Any(), gomock.Any()). - Return(tt.resp, nil). - Times(1) + const ttl = 20 * time.Millisecond - 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 := setupTestAPI(seqData) + api := initTestAPI(seqData) - httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getFieldsResponse]{ - Method: http.MethodGet, - Target: "/seqapi/v1/fields", - Handler: api.serveGetFields, - Want: tt.want, - }) + 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, + }) - time.Sleep(ttl / 2) + time.Sleep(ttl / 2) - httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getFieldsResponse]{ - Method: http.MethodGet, - Target: "/seqapi/v1/fields", - Handler: api.serveGetFields, - Want: tt.want, - }) + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: httptest.NewRequest(http.MethodGet, "/seqapi/v1/fields", http.NoBody), + Handler: api.serveGetFields, + WantRespBody: testCase.wantRespBody, + WantStatus: http.StatusOK, }) + + time.Sleep(ttl) } } @@ -232,8 +205,8 @@ func TestServeGetPinnedFields(t *testing.T) { tests := []struct { name string - fields []config.Field - want getFieldsResponse + fields []config.Field + wantRespBody string }{ { name: "ok", @@ -241,16 +214,15 @@ func TestServeGetPinnedFields(t *testing.T) { {Name: "field1", Type: "keyword"}, {Name: "field2", Type: "text"}, }, - want: getFieldsResponse{ - Fields: fields{ - {Name: "field1", Type: "keyword"}, - {Name: "field2", Type: "text"}, - }, - }, + wantRespBody: `{"fields":[{"name":"field1","type":"keyword"},{"name":"field2","type":"text"}]}`, + }, + { + name: "empty", + wantRespBody: `{"fields":[]}`, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -262,14 +234,27 @@ func TestServeGetPinnedFields(t *testing.T) { }, } - api := setupTestAPI(seqData) + api := initTestAPI(seqData) + req := httptest.NewRequest(http.MethodGet, "/seqapi/v1/fields/pinned", http.NoBody) + w := httptest.NewRecorder() - httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getFieldsResponse]{ - Method: http.MethodGet, - Target: "/seqapi/v1/fields/pinned", - Handler: api.serveGetPinnedFields, - Want: tt.want, - }) + api.serveGetPinnedFields(w, req) + + resp := w.Result() + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + + 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) + } }) } } 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 5934a98..1811ae6 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,6 +2,7 @@ package http import ( "net/http" + "net/http/httptest" "strings" "testing" "time" @@ -12,106 +13,68 @@ import ( "github.com/ozontech/seq-ui/internal/api/httputil" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" - mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" + "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" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) func TestServeGetAsyncSearchesList(t *testing.T) { - 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" + 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) + ) type mockArgs struct { - req *seqapi.GetAsyncSearchesListRequest - resp *seqapi.GetAsyncSearchesListResponse - err error + searchIDs []string + proxyReq *seqapi.GetAsyncSearchesListRequest + proxyResp *seqapi.GetAsyncSearchesListResponse + proxyErr error + + repoReq types.GetAsyncSearchesListRequest + repoResp []types.AsyncSearchInfo + repoErr error } tests := []struct { name string - req getAsyncSearchesListRequest - want getAsyncSearchesListResponse - wantErr bool + reqBody string + cfg config.Handlers + wantRespBody string + wantStatus int mockArgs *mockArgs }{ { - 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, - }, - }), + name: "ok_no_filters", + reqBody: `{}`, mockArgs: &mockArgs{ - req: &seqapi.GetAsyncSearchesListRequest{}, - resp: &seqapi.GetAsyncSearchesListResponse{ + proxyReq: &seqapi.GetAsyncSearchesListRequest{}, + proxyResp: &seqapi.GetAsyncSearchesListResponse{ Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchId: testSearchID, + SearchId: mockSearchID1, 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), + From: timestamppb.New(mockTime.Add(-15 * time.Minute)), + To: timestamppb.New(mockTime), WithDocs: true, Size: 100, }, - StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, OwnerName: mockUserName1, @@ -123,8 +86,8 @@ func TestServeGetAsyncSearchesList(t *testing.T) { 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), + From: timestamppb.New(mockTime.Add(-1 * time.Hour)), + To: timestamppb.New(mockTime), Aggs: []*seqapi.AggregationQuery{ { Field: "x", @@ -138,9 +101,9 @@ func TestServeGetAsyncSearchesList(t *testing.T) { }, WithDocs: false, }, - StartedAt: timestamppb.New(testTimestamp.Add(-60 * time.Second)), - ExpiresAt: timestamppb.New(testTimestamp.Add(300 * time.Second)), - CanceledAt: timestamppb.New(testTimestamp), + StartedAt: timestamppb.New(mockTime.Add(-60 * time.Second)), + ExpiresAt: timestamppb.New(mockTime.Add(300 * time.Second)), + CanceledAt: timestamppb.New(mockTime), Progress: 1, DiskUsage: 256, OwnerName: mockUserName2, @@ -150,62 +113,49 @@ func TestServeGetAsyncSearchesList(t *testing.T) { Code: seqapi.ErrorCode_ERROR_CODE_NO, }, }, - }, - }, - { - name: "ok_filters", - req: getAsyncSearchesListRequest{ - Status: &statusDone, - Limit: 10, - Offset: 20, - Owner: &mockUserName1, - }, - want: getAsyncSearchesListResponseFromProto(&seqapi.GetAsyncSearchesListResponse{ - Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ + repoReq: types.GetAsyncSearchesListRequest{}, + repoResp: []types.AsyncSearchInfo{ { - 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, + SearchID: mockSearchID1, + OwnerID: mockProfileID1, OwnerName: mockUserName1, }, + { + SearchID: mockSearchID2, + OwnerID: mockProfileID2, + OwnerName: mockUserName2, + }, }, - Error: &seqapi.Error{ - Code: seqapi.ErrorCode_ERROR_CODE_NO, - }, - }), + 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"}`, mockArgs: &mockArgs{ - req: &seqapi.GetAsyncSearchesListRequest{ + proxyReq: &seqapi.GetAsyncSearchesListRequest{ Status: seqapi.AsyncSearchStatus_ASYNC_SEARCH_STATUS_DONE.Enum(), OwnerName: &mockUserName1, Limit: 10, Offset: 20, }, - resp: &seqapi.GetAsyncSearchesListResponse{ + proxyResp: &seqapi.GetAsyncSearchesListResponse{ Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchId: testSearchID, + SearchId: mockSearchID1, 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), + From: timestamppb.New(mockTime.Add(-15 * time.Minute)), + To: timestamppb.New(mockTime), WithDocs: true, Size: 100, }, - StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, OwnerName: mockUserName1, @@ -215,53 +165,41 @@ func TestServeGetAsyncSearchesList(t *testing.T) { Code: seqapi.ErrorCode_ERROR_CODE_NO, }, }, - }, - }, - { - name: "partial_response", - req: getAsyncSearchesListRequest{}, - want: getAsyncSearchesListResponseFromProto(&seqapi.GetAsyncSearchesListResponse{ - Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ + repoReq: types.GetAsyncSearchesListRequest{ + Owner: &mockUserName1, + }, + repoResp: []types.AsyncSearchInfo{ { - 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, + SearchID: mockSearchID1, + OwnerID: mockProfileID1, OwnerName: mockUserName1, }, }, - Error: &seqapi.Error{ - Code: seqapi.ErrorCode_ERROR_CODE_PARTIAL_RESPONSE, - Message: "partial response", - }, - }), + 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: `{}`, mockArgs: &mockArgs{ - req: &seqapi.GetAsyncSearchesListRequest{}, - resp: &seqapi.GetAsyncSearchesListResponse{ + proxyReq: &seqapi.GetAsyncSearchesListRequest{}, + proxyResp: &seqapi.GetAsyncSearchesListResponse{ Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchId: testSearchID, + SearchId: mockSearchID1, 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), + From: timestamppb.New(mockTime.Add(-15 * time.Minute)), + To: timestamppb.New(mockTime), WithDocs: true, Size: 100, }, - StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, OwnerName: mockUserName1, @@ -272,69 +210,56 @@ 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", - req: getAsyncSearchesListRequest{ - Limit: -10, - Offset: 20, - }, - wantErr: true, + name: "err_limit", + reqBody: `{"limit":-10,"offset":20}`, + wantRespBody: `{"message":"invalid request field: 'limit' must be non-negative"}`, + wantStatus: http.StatusBadRequest, }, { - name: "err_offset", - req: getAsyncSearchesListRequest{ - Limit: 10, - Offset: -20, - }, - wantErr: true, + name: "err_offset", + reqBody: `{"limit":10,"offset":-20}`, + wantRespBody: `{"message":"invalid request field: 'offset' must be non-negative"}`, + wantStatus: http.StatusBadRequest, }, { - 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", - }, - }), + name: "err_invalid_request", + reqBody: "invalid-request", + wantStatus: http.StatusBadRequest, + }, + { + name: "query_too_long", + reqBody: "{}", mockArgs: &mockArgs{ - req: &seqapi.GetAsyncSearchesListRequest{}, - resp: &seqapi.GetAsyncSearchesListResponse{ + proxyReq: &seqapi.GetAsyncSearchesListRequest{}, + proxyResp: &seqapi.GetAsyncSearchesListResponse{ Searches: []*seqapi.GetAsyncSearchesListResponse_ListItem{ { - SearchId: testSearchID, + SearchId: mockSearchID1, 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), + From: timestamppb.New(mockTime.Add(-15 * time.Minute)), + To: timestamppb.New(mockTime), WithDocs: true, Size: 100, }, - StartedAt: timestamppb.New(testTimestamp.Add(-30 * time.Second)), - ExpiresAt: timestamppb.New(testTimestamp.Add(30 * time.Second)), + StartedAt: timestamppb.New(mockTime.Add(-30 * time.Second)), + ExpiresAt: timestamppb.New(mockTime.Add(30 * time.Second)), Progress: 1, DiskUsage: 512, OwnerName: mockUserName1, @@ -345,35 +270,52 @@ 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() - ctrl := gomock.NewController(t) - svcMock := mock_asyncsearches.NewMockService(ctrl) - seqData := test.APITestData{} - seqData.Mocks.AsyncSearchesSvc = svcMock + seqData := test.APITestData{ + AsyncCfg: config.AsyncSearch{ + ListQueryLengthLimit: 1000, + }, + } if tt.mockArgs != nil { - svcMock.EXPECT(). - GetAsyncSearchesList(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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 } - api := setupTestAPI(seqData) + api := initTestAPIWithAsyncSearches(seqData) + req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/async_search/list", strings.NewReader(tt.reqBody)) - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveGetAsyncSearchesList, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } @@ -381,12 +323,13 @@ func TestServeGetAsyncSearchesList(t *testing.T) { func TestServeGetAsyncSearchesList_Disabled(t *testing.T) { seqData := test.APITestData{} - api := setupTestAPI(seqData) + api := initTestAPI(seqData) + req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/async_search/list", strings.NewReader("{}")) - httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[getAsyncSearchesListRequest, struct{}]{ - Method: http.MethodPost, - Target: "/seqapi/v1/async_search/list", - Handler: api.serveGetAsyncSearchesList, - WantErr: true, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveGetAsyncSearchesList, + WantRespBody: `{"message":"async searches disabled"}`, + WantStatus: http.StatusBadRequest, }) } diff --git a/internal/api/seqapi/v1/http/get_envs_test.go b/internal/api/seqapi/v1/http/get_envs_test.go index 3ede5d5..e2f54a6 100644 --- a/internal/api/seqapi/v1/http/get_envs_test.go +++ b/internal/api/seqapi/v1/http/get_envs_test.go @@ -1,19 +1,22 @@ package http import ( + "encoding/json" "net/http" + "net/http/httptest" "testing" - "github.com/ozontech/seq-ui/internal/api/httputil" + "github.com/stretchr/testify/require" + "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 - want getEnvsResponse + name string + cfg config.SeqAPI + wantEnvs []envInfo }{ { name: "single_env", @@ -26,16 +29,14 @@ func TestServeGetEnvs(t *testing.T) { SeqCLIMaxSearchLimit: 10000, }, }, - want: getEnvsResponse{ - []envInfo{ - { - Env: "", - MaxSearchLimit: 100, - MaxExportLimit: 200, - MaxParallelExportRequests: 2, - MaxAggregationsPerRequest: 5, - SeqCliMaxSearchLimit: 10000, - }, + wantEnvs: []envInfo{ + { + Env: "", + MaxSearchLimit: 100, + MaxExportLimit: 200, + MaxParallelExportRequests: 2, + MaxAggregationsPerRequest: 5, + SeqCliMaxSearchLimit: 10000, }, }, }, @@ -97,54 +98,53 @@ func TestServeGetEnvs(t *testing.T) { }, DefaultEnv: "cluster-10", }, - 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, - }, + 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, }, }, }, } for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -152,14 +152,16 @@ func TestServeGetEnvs(t *testing.T) { Cfg: tt.cfg, } - api := setupTestAPI(seqData) + api := initTestAPI(seqData) + + req := httptest.NewRequest(http.MethodGet, "/seqapi/v1/envs", http.NoBody) + w := httptest.NewRecorder() + api.serveGetEnvs(w, req) - httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getEnvsResponse]{ - Method: http.MethodGet, - Target: "/seqapi/v1/envs", - Handler: api.serveGetEnvs, - Want: tt.want, - }) + 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") }) } } diff --git a/internal/api/seqapi/v1/http/histogram_test.go b/internal/api/seqapi/v1/http/histogram_test.go index e0efedb..ea5be57 100644 --- a/internal/api/seqapi/v1/http/histogram_test.go +++ b/internal/api/seqapi/v1/http/histogram_test.go @@ -2,7 +2,10 @@ package http import ( "errors" + "fmt" "net/http" + "net/http/httptest" + "strings" "testing" "time" @@ -16,6 +19,15 @@ 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 @@ -25,35 +37,20 @@ func TestServeGetHistogram(t *testing.T) { tests := []struct { name string - req getHistogramRequest - want getHistogramResponse - wantErr bool + reqBody string + wantRespBody string + wantStatus int mockArgs *mockArgs }{ { - 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, - }, + name: "ok", + reqBody: formatReqBody("5s"), mockArgs: &mockArgs{ req: &seqapi.GetHistogramRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Interval: "5s", }, resp: &seqapi.GetHistogramResponse{ @@ -63,27 +60,17 @@ 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", - 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, - }, + name: "err_partial_response", + reqBody: formatReqBody("10s"), mockArgs: &mockArgs{ req: &seqapi.GetHistogramRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Interval: "10s", }, resp: &seqapi.GetHistogramResponse{ @@ -94,29 +81,31 @@ 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_client", - req: getHistogramRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - Interval: "20s", - }, - wantErr: true, + name: "err_invalid_request", + reqBody: "invalid-request", + wantStatus: http.StatusBadRequest, + }, + { + name: "err_client", + reqBody: formatReqBody("20s"), mockArgs: &mockArgs{ req: &seqapi.GetHistogramRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), 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() @@ -124,25 +113,22 @@ 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 := mock_seqdb.NewMockClient(ctrl) + seqDbMock.EXPECT().GetHistogram(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) seqData.Mocks.SeqDB = seqDbMock } - api := setupTestAPI(seqData) + api := initTestAPI(seqData) + req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/histogram", strings.NewReader(tt.reqBody)) - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveGetHistogram, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } diff --git a/internal/api/seqapi/v1/http/limits_test.go b/internal/api/seqapi/v1/http/limits_test.go index cf339e4..28d3748 100644 --- a/internal/api/seqapi/v1/http/limits_test.go +++ b/internal/api/seqapi/v1/http/limits_test.go @@ -2,6 +2,7 @@ package http import ( "net/http" + "net/http/httptest" "testing" "github.com/ozontech/seq-ui/internal/api/httputil" @@ -11,11 +12,10 @@ import ( func TestServeGetLimits(t *testing.T) { tests := []struct { - name string - - env string - cfg config.SeqAPI - want getLimitsResponse + name string + env string + cfg config.SeqAPI + wantRespBody string }{ { name: "ok", @@ -29,22 +29,16 @@ func TestServeGetLimits(t *testing.T) { SeqCLIMaxSearchLimit: 10000, }, }, - want: getLimitsResponse{ - MaxSearchLimit: 100, - MaxExportLimit: 200, - MaxParallelExportRequests: 2, - MaxAggregationsPerRequest: 5, - SeqCliMaxSearchLimit: 10000, - }, + wantRespBody: `{"maxSearchLimit":100,"maxExportLimit":200,"maxParallelExportRequests":2,"maxAggregationsPerRequest":5,"seqCliMaxSearchLimit":10000}`, }, { - name: "empty", - env: "default", - want: getLimitsResponse{}, + name: "empty", + env: "default", + wantRespBody: `{"maxSearchLimit":0,"maxExportLimit":0,"maxParallelExportRequests":0,"maxAggregationsPerRequest":0,"seqCliMaxSearchLimit":0}`, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -52,13 +46,14 @@ func TestServeGetLimits(t *testing.T) { Cfg: tt.cfg, } - api := setupTestAPI(seqData) + api := initTestAPI(seqData) + req := httptest.NewRequest(http.MethodGet, "/seqapi/v1/limits", http.NoBody) - httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getLimitsResponse]{ - Method: http.MethodGet, - Target: "/seqapi/v1/limits", - Handler: api.serveGetLimits, - Want: tt.want, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveGetLimits, + WantRespBody: tt.wantRespBody, + WantStatus: http.StatusOK, }) }) } diff --git a/internal/api/seqapi/v1/http/logs_lifespan_test.go b/internal/api/seqapi/v1/http/logs_lifespan_test.go index 0ed4b5c..a3d4401 100644 --- a/internal/api/seqapi/v1/http/logs_lifespan_test.go +++ b/internal/api/seqapi/v1/http/logs_lifespan_test.go @@ -1,7 +1,9 @@ package http import ( + "errors" "net/http" + "net/http/httptest" "strconv" "testing" "time" @@ -20,11 +22,13 @@ import ( ) func TestServeGetLogsLifespan(t *testing.T) { - var ( - resultStr = "36000" // 10(h) * 60(min/h) * 60(sec/min) - cacheKey = "logs_lifespan" - result = 10 * time.Hour - cacheTTL = time.Minute + 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}` ) unparsable := func(s string) bool { @@ -32,6 +36,8 @@ func TestServeGetLogsLifespan(t *testing.T) { return err != nil } + oldestStorageTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + tests := []struct { name string @@ -41,15 +47,16 @@ func TestServeGetLogsLifespan(t *testing.T) { clientResp *seqapi.StatusResponse clientErr error - wantErr bool - want getLogsLifespanResponse + wantStatus int + wantRespBody string }{ { name: "ok_cached", getOp: test.CacheMockArgs{ Value: resultStr, }, - want: getLogsLifespanResponse{Lifespan: 36000}, + wantStatus: http.StatusOK, + wantRespBody: resultRespBody, }, { name: "ok_cached_unparsable", @@ -60,9 +67,10 @@ func TestServeGetLogsLifespan(t *testing.T) { Value: resultStr, }, clientResp: &seqapi.StatusResponse{ - OldestStorageTime: timestamppb.New(testTimestamp), + OldestStorageTime: timestamppb.New(oldestStorageTime), }, - want: getLogsLifespanResponse{Lifespan: 36000}, + wantStatus: http.StatusOK, + wantRespBody: resultRespBody, }, { name: "ok_no_cached", @@ -73,17 +81,18 @@ func TestServeGetLogsLifespan(t *testing.T) { Value: resultStr, }, clientResp: &seqapi.StatusResponse{ - OldestStorageTime: timestamppb.New(testTimestamp), + OldestStorageTime: timestamppb.New(oldestStorageTime), }, - want: getLogsLifespanResponse{Lifespan: 36000}, + wantStatus: http.StatusOK, + wantRespBody: resultRespBody, }, { name: "err_client", getOp: test.CacheMockArgs{ Err: cache.ErrNotFound, }, - clientErr: errSomethingWrong, - wantErr: true, + clientErr: errors.New("network error"), + wantStatus: http.StatusInternalServerError, }, { name: "err_nil_oldest_storage_time", @@ -93,11 +102,11 @@ func TestServeGetLogsLifespan(t *testing.T) { clientResp: &seqapi.StatusResponse{ OldestStorageTime: nil, }, - wantErr: true, + wantStatus: http.StatusInternalServerError, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -109,43 +118,37 @@ func TestServeGetLogsLifespan(t *testing.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 := mock_cache.NewMockCache(ctrl) + 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 := setupTestAPI(seqData) + api := initTestAPI(seqData) api.nowFn = func() time.Time { - return testTimestamp.Add(result) + return oldestStorageTime.Add(result) } - httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getLogsLifespanResponse]{ - Method: http.MethodGet, - Target: "/seqapi/v1/logs_lifespan", - Handler: api.serveGetLogsLifespan, - Want: tt.want, - WantErr: tt.wantErr, + 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, }) }) } diff --git a/internal/api/seqapi/v1/http/search_test.go b/internal/api/seqapi/v1/http/search_test.go index c22272f..1dc3932 100644 --- a/internal/api/seqapi/v1/http/search_test.go +++ b/internal/api/seqapi/v1/http/search_test.go @@ -1,10 +1,16 @@ 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" @@ -17,9 +23,34 @@ import ( ) func TestServeSearch(t *testing.T) { - var ( - eventTime = testTimestamp.Add(time.Millisecond) - ) + 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() + } type mockArgs struct { req *seqapi.SearchRequest @@ -30,36 +61,21 @@ func TestServeSearch(t *testing.T) { tests := []struct { name string - req searchRequest - want searchResponse - cfg config.SeqAPI - wantErr bool + reqBody string + wantRespBody string + wantStatus int mockArgs *mockArgs + cfg config.SeqAPI }{ { - 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, - }, - }), + name: "ok_simple", + reqBody: formatReqBody(3, 0, false, "", nil, ""), mockArgs: &mockArgs{ req: &seqapi.SearchRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Limit: 3, Offset: 0, }, @@ -70,32 +86,22 @@ func TestServeSearch(t *testing.T) { }, }, }, - }, - { - 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, - }, + 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, 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: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Limit: 3, Offset: 0, WithTotal: true, @@ -108,31 +114,22 @@ func TestServeSearch(t *testing.T) { }, }, }, - }, - { - 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, - }, + 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, 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: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Limit: 3, Offset: 0, Order: seqapi.Order_ORDER_ASC, @@ -144,34 +141,22 @@ func TestServeSearch(t *testing.T) { }, }, }, - }, - { - 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, - }, + 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, 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: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Limit: 3, Offset: 0, Histogram: &seqapi.SearchRequest_Histogram{ @@ -186,40 +171,28 @@ func TestServeSearch(t *testing.T) { }, }, }, - }, - { - 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, - }, + 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, 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: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Limit: 3, Offset: 0, Aggregations: []*seqapi.AggregationQuery{ @@ -240,30 +213,22 @@ func TestServeSearch(t *testing.T) { }, }, }, - }, - { - 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, - }, + 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, 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: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Limit: 3, Offset: 0, }, @@ -274,30 +239,22 @@ func TestServeSearch(t *testing.T) { }, }, }, - }, - { - 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, - }, + wantRespBody: `{"events":[],"error":{"code":"ERROR_CODE_NO"},"partialResponse":false}`, + wantStatus: http.StatusOK, 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: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Limit: 3, Offset: 0, }, @@ -310,92 +267,63 @@ 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_search_limit_zero", - req: searchRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - Limit: 0, - }, - wantErr: true, + name: "err_invalid_request", + reqBody: "invalid-request", + wantStatus: http.StatusBadRequest, }, { - name: "err_search_limit_max", - req: searchRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - Limit: 10, - }, + name: "err_search_limit_zero", + reqBody: formatReqBody(0, 0, false, "", nil, ""), + wantStatus: http.StatusBadRequest, + }, + { + name: "err_search_limit_max", + reqBody: formatReqBody(10, 0, false, "", nil, ""), + wantStatus: http.StatusBadRequest, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxSearchLimit: 5, }, }, - wantErr: true, }, { - name: "err_aggs_limit_max", - req: searchRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - Limit: 3, - Aggregations: aggregationQueries{{}, {}, {}}, - }, + name: "err_aggs_limit_max", + reqBody: formatReqBody(3, 0, false, "", aggregationQueries{{}, {}, {}}, ""), + wantStatus: http.StatusBadRequest, cfg: config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxSearchLimit: 5, MaxAggregationsPerRequest: 2, }, }, - wantErr: true, - }, - { - name: "err_offset_too_high", - req: searchRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - Limit: 3, - Offset: 11, - }, + }, { + name: "err_offset_too_high", + reqBody: formatReqBody(3, 11, false, "", nil, ""), + wantStatus: http.StatusBadRequest, cfg: test.SetCfgDefaults(config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ MaxSearchLimit: 5, MaxSearchOffsetLimit: 10, }, }), - wantErr: true, }, { - 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, - }, - }), + name: "err_total_too_high", + reqBody: formatReqBody(3, 0, true, "", nil, ""), mockArgs: &mockArgs{ req: &seqapi.SearchRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Limit: 3, Offset: 0, WithTotal: true, @@ -409,35 +337,38 @@ func TestServeSearch(t *testing.T) { }, }, }, - }, - { - name: "err_client", - req: searchRequest{ - Query: testQuery, - From: testTimestamp, - To: testTimestamp.Add(time.Second), - Limit: 3, - }, + 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, cfg: test.SetCfgDefaults(config.SeqAPI{ SeqAPIOptions: &config.SeqAPIOptions{ - MaxSearchLimit: 5, + MaxSearchLimit: 5, + MaxSearchOffsetLimit: 10, }, }), - wantErr: true, + }, + { + name: "err_client", + reqBody: formatReqBody(3, 0, false, "", nil, ""), mockArgs: &mockArgs{ req: &seqapi.SearchRequest{ - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), Limit: 3, Offset: 0, }, - err: errSomethingWrong, + err: errors.New("client error"), }, + 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() @@ -449,23 +380,20 @@ 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 := setupTestAPI(seqData) + api := initTestAPI(seqData) + req := httptest.NewRequest(http.MethodPost, "/seqapi/v1/search", strings.NewReader(tt.reqBody)) - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveSearch, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } diff --git a/internal/api/seqapi/v1/http/start_async_search.go b/internal/api/seqapi/v1/http/start_async_search.go index 1096ec8..0904cc1 100644 --- a/internal/api/seqapi/v1/http/start_async_search.go +++ b/internal/api/seqapi/v1/http/start_async_search.go @@ -103,7 +103,13 @@ func (a *API) serveStartAsyncSearch(w http.ResponseWriter, r *http.Request) { span.SetAttributes(spanAttributes...) - resp, err := a.asyncSearches.StartAsyncSearch(ctx, httpReq.toProto(parsedRetention)) + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + httputil.ProcessError(wr, err) + return + } + + resp, err := a.asyncSearches.StartAsyncSearch(ctx, profileID, 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 8541d72..d1ac82d 100644 --- a/internal/api/seqapi/v1/http/start_async_search_test.go +++ b/internal/api/seqapi/v1/http/start_async_search_test.go @@ -1,7 +1,11 @@ package http import ( + "context" + "fmt" "net/http" + "net/http/httptest" + "strings" "testing" "time" @@ -11,64 +15,60 @@ import ( "github.com/ozontech/seq-ui/internal/api/httputil" "github.com/ozontech/seq-ui/internal/api/seqapi/v1/test" - mock_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" + "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" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) func TestServeStartAsyncSearch(t *testing.T) { - var ( - meta = `{"some":"meta"}` + const ( + mockSearchID = "c9a34cf8-4c66-484e-9cc2-42979d848656" + mockUserName = "some_user" + mockProfileID = 1 + 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 { - req *seqapi.StartAsyncSearchRequest - resp *seqapi.StartAsyncSearchResponse - err error + proxyReq *seqapi.StartAsyncSearchRequest + proxyResp *seqapi.StartAsyncSearchResponse + proxyErr error + + profilesReq types.GetOrCreateUserProfileRequest + profilesResp types.UserProfile + profilesErr error + + repoReq types.SaveAsyncSearchRequest + repoErr error } tests := []struct { name string - req startAsyncSearchRequest - want startAsyncSearchResponse - wantErr bool + reqBody string + wantRespBody string + wantStatus int mockArgs *mockArgs }{ { - 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, - }, + name: "ok", + reqBody: formatReqBody("60s"), mockArgs: &mockArgs{ - req: &seqapi.StartAsyncSearchRequest{ + proxyReq: &seqapi.StartAsyncSearchRequest{ Retention: durationpb.New(60 * time.Second), - Query: testQuery, - From: timestamppb.New(testTimestamp), - To: timestamppb.New(testTimestamp.Add(time.Second)), + Query: query, + From: timestamppb.New(from), + To: timestamppb.New(to), WithDocs: true, Size: 100, Hist: &seqapi.StartAsyncSearchRequest_HistQuery{ @@ -85,64 +85,31 @@ func TestServeStartAsyncSearch(t *testing.T) { }, Meta: meta, }, - resp: &seqapi.StartAsyncSearchResponse{ - SearchId: testSearchID, + proxyResp: &seqapi.StartAsyncSearchResponse{ + SearchId: mockSearchID, }, - }, - }, - { - 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", + profilesReq: types.GetOrCreateUserProfileRequest{ + UserName: mockUserName, }, - Aggregations: aggregationTsQueries{ - { - aggregationQuery: aggregationQuery{ - Field: "v", - GroupBy: "level", - Func: afAvg, - Quantiles: []float64{0.95}, - }, - Interval: "30s", - }, + profilesResp: types.UserProfile{ + ID: mockProfileID, + UserName: mockUserName, }, - }, - 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, + repoReq: types.SaveAsyncSearchRequest{ + SearchID: mockSearchID, + OwnerID: mockProfileID, + 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() @@ -151,25 +118,32 @@ func TestServeStartAsyncSearch(t *testing.T) { if tt.mockArgs != nil { ctrl := gomock.NewController(t) - svcMock := mock_asyncsearches.NewMockService(ctrl) - svcMock.EXPECT(). - StartAsyncSearch(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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 - seqData.Mocks.AsyncSearchesSvc = svcMock + 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 } - api := setupTestAPI(seqData) + 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)) - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveStartAsyncSearch, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } @@ -177,12 +151,13 @@ func TestServeStartAsyncSearch(t *testing.T) { func TestServeStartAsyncSearch_Disabled(t *testing.T) { seqData := test.APITestData{} - api := setupTestAPI(seqData) - - httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[startAsyncSearchRequest, struct{}]{ - Method: http.MethodPost, - Target: "/seqapi/v1/async_search/start", - Handler: api.serveStartAsyncSearch, - WantErr: true, + 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, }) } diff --git a/internal/api/seqapi/v1/http/test_data.go b/internal/api/seqapi/v1/http/test_data.go index 0f24d93..455b472 100644 --- a/internal/api/seqapi/v1/http/test_data.go +++ b/internal/api/seqapi/v1/http/test_data.go @@ -2,27 +2,17 @@ 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" ) -// 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 { +func initTestAPI(data test.APITestData) *API { // when test cases don't explicitly provide configuration. if data.Cfg.SeqAPIOptions == nil { data.Cfg.SeqAPIOptions = &config.SeqAPIOptions{} @@ -34,19 +24,20 @@ func setupTestAPI(data test.APITestData) *API { seqDBClients[envConfig.SeqDB] = data.Mocks.SeqDB } - 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) + return New(data.Cfg, seqDBClients, data.Mocks.Cache, data.Mocks.Cache, nil, nil) } -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) +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, } + 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 f1a08c4..9cdae47 100644 --- a/internal/api/seqapi/v1/seqapi.go +++ b/internal/api/seqapi/v1/seqapi.go @@ -3,6 +3,7 @@ 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" @@ -21,11 +22,12 @@ func New( seqDB map[string]seqdb.Client, inmemWithRedisCache cache.Cache, redisCache cache.Cache, - asyncSearches asyncsearches.Service, + asyncSearches *asyncsearches.Service, + p *profiles.Profiles, ) *SeqAPI { return &SeqAPI{ - grpcAPI: grpc_api.New(cfg, seqDB, inmemWithRedisCache, redisCache, asyncSearches), - httpAPI: http_api.New(cfg, seqDB, inmemWithRedisCache, redisCache, asyncSearches), + grpcAPI: grpc_api.New(cfg, seqDB, inmemWithRedisCache, redisCache, asyncSearches, p), + httpAPI: http_api.New(cfg, seqDB, inmemWithRedisCache, redisCache, asyncSearches, p), } } diff --git a/internal/api/seqapi/v1/test/data.go b/internal/api/seqapi/v1/test/data.go index 0ab1d44..46ea63d 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_asyncsearches "github.com/ozontech/seq-ui/internal/pkg/service/async_searches/mock" + mock_repo "github.com/ozontech/seq-ui/internal/pkg/repository/mock" "github.com/ozontech/seq-ui/pkg/seqapi/v1" ) @@ -20,14 +20,16 @@ type CacheMockArgs struct { } type Mocks struct { - SeqDB *mock_seqdb.MockClient - Cache *mock_cache.MockCache - AsyncSearchesSvc *mock_asyncsearches.MockService + SeqDB *mock_seqdb.MockClient + Cache *mock_cache.MockCache + AsyncSearchesRepo *mock_repo.MockAsyncSearches + ProfilesRepo *mock_repo.MockUserProfiles } type APITestData struct { - Cfg config.SeqAPI - Mocks Mocks + Cfg config.SeqAPI + AsyncCfg config.AsyncSearch + 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 c3ff3c6..202863d 100644 --- a/internal/api/userprofile/v1/grpc/api.go +++ b/internal/api/userprofile/v1/grpc/api.go @@ -1,18 +1,21 @@ package grpc import ( - "github.com/ozontech/seq-ui/internal/pkg/service/userprofile" - api "github.com/ozontech/seq-ui/pkg/userprofile/v1" + "github.com/ozontech/seq-ui/internal/api/profiles" + "github.com/ozontech/seq-ui/internal/pkg/service" + "github.com/ozontech/seq-ui/pkg/userprofile/v1" ) type API struct { - api.UnimplementedUserProfileServiceServer + userprofile.UnimplementedUserProfileServiceServer - service userprofile.Service + service service.Service + profiles *profiles.Profiles } -func New(svc userprofile.Service) *API { +func New(svc service.Service, p *profiles.Profiles) *API { return &API{ - service: svc, + service: svc, + profiles: p, } } diff --git a/internal/api/userprofile/v1/grpc/favorite_queries.go b/internal/api/userprofile/v1/grpc/favorite_queries.go index 32b2631..55a1f43 100644 --- a/internal/api/userprofile/v1/grpc/favorite_queries.go +++ b/internal/api/userprofile/v1/grpc/favorite_queries.go @@ -16,8 +16,14 @@ func (a *API) GetFavoriteQueries(ctx context.Context, _ *userprofile.GetFavorite ctx, span := tracing.StartSpan(ctx, "userprofile_v1_get_favorite_queries") defer span.End() - request := types.GetFavoriteQueriesRequest{} + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + return nil, grpcutil.ProcessError(err) + } + request := types.GetFavoriteQueriesRequest{ + ProfileID: profileID, + } favoriteQueries, err := a.service.GetFavoriteQueries(ctx, request) if err != nil { return nil, grpcutil.ProcessError(err) @@ -48,15 +54,21 @@ func (a *API) CreateFavoriteQuery(ctx context.Context, req *userprofile.CreateFa }, ) - request := types.GetOrCreateFavoriteQueryRequest{Query: req.Query} + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + return nil, grpcutil.ProcessError(err) + } + 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) @@ -77,9 +89,16 @@ func (a *API) DeleteFavoriteQuery(ctx context.Context, req *userprofile.DeleteFa Value: attribute.Int64Value(req.GetId()), }) - request := types.DeleteFavoriteQueryRequest{ID: req.Id} + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + return nil, grpcutil.ProcessError(err) + } - if err := a.service.DeleteFavoriteQuery(ctx, request); err != nil { + request := types.DeleteFavoriteQueryRequest{ + ID: req.Id, + ProfileID: profileID, + } + 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 805ba84..b2e4f52 100644 --- a/internal/api/userprofile/v1/grpc/favorite_queries_test.go +++ b/internal/api/userprofile/v1/grpc/favorite_queries_test.go @@ -2,6 +2,7 @@ package grpc import ( "context" + "errors" "testing" "github.com/stretchr/testify/require" @@ -14,10 +15,10 @@ import ( ) func TestGetFavoriteQueries(t *testing.T) { - var ( - relativeFrom uint64 = 300 - queryName = "my query" - ) + userName := "unnamed" + var profileID int64 = 1 + queryName := "my query" + var relativeFrom uint64 = 300 type mockArgs struct { req types.GetFavoriteQueriesRequest @@ -32,9 +33,10 @@ func TestGetFavoriteQueries(t *testing.T) { wantCode codes.Code mockArgs *mockArgs + noUser bool }{ { - name: "ok", + name: "success", want: &userprofile.GetFavoriteQueriesResponse{ Queries: []*userprofile.GetFavoriteQueriesResponse_Query{ { @@ -63,6 +65,9 @@ func TestGetFavoriteQueries(t *testing.T) { }, wantCode: codes.OK, mockArgs: &mockArgs{ + req: types.GetFavoriteQueriesRequest{ + ProfileID: profileID, + }, resp: types.FavoriteQueries{ { ID: 1, @@ -90,28 +95,40 @@ func TestGetFavoriteQueries(t *testing.T) { }, }, { - name: "err_svc", + name: "err_no_user", + wantCode: codes.Unauthenticated, + noUser: true, + }, + { + name: "err_repo_random", wantCode: codes.Internal, mockArgs: &mockArgs{ - err: errSomethingWrong, + req: types.GetFavoriteQueriesRequest{ + ProfileID: profileID, + }, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newFavoriteQueriesTestData(t) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - GetFavoriteQueries(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + mockedRepo.EXPECT().GetAll(gomock.Any(), tt.mockArgs.req). + Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) } - got, err := api.GetFavoriteQueries(context.Background(), &userprofile.GetFavoriteQueriesRequest{}) + 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{}) require.Equal(t, tt.wantCode, status.Code(err)) if tt.wantCode != codes.OK { @@ -124,12 +141,12 @@ func TestGetFavoriteQueries(t *testing.T) { } func TestCreateFavoriteQuery(t *testing.T) { - var ( - queryID int64 = 1 - relativeFrom uint64 = 300 - query = "test" - queryName = "my query" - ) + userName := "unnamed" + var profileID int64 = 1 + var queryID int64 = 1 + query := "test" + queryName := "my query" + var relativeFrom uint64 = 300 type mockArgs struct { req types.GetOrCreateFavoriteQueryRequest @@ -145,9 +162,10 @@ func TestCreateFavoriteQuery(t *testing.T) { wantCode codes.Code mockArgs *mockArgs + noUser bool }{ { - name: "ok", + name: "success", req: &userprofile.CreateFavoriteQueryRequest{ Query: query, Name: &queryName, @@ -159,6 +177,7 @@ func TestCreateFavoriteQuery(t *testing.T) { wantCode: codes.OK, mockArgs: &mockArgs{ req: types.GetOrCreateFavoriteQueryRequest{ + ProfileID: profileID, Query: query, Name: queryName, RelativeFrom: relativeFrom, @@ -167,32 +186,66 @@ func TestCreateFavoriteQuery(t *testing.T) { }, }, { - name: "err_svc", + 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", req: &userprofile.CreateFavoriteQueryRequest{ Query: query, }, wantCode: codes.Internal, mockArgs: &mockArgs{ - req: types.GetOrCreateFavoriteQueryRequest{Query: query}, - err: errSomethingWrong, + req: types.GetOrCreateFavoriteQueryRequest{ + ProfileID: profileID, + Query: query, + }, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newFavoriteQueriesTestData(t) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - GetOrCreateFavoriteQuery(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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) } - got, err := api.CreateFavoriteQuery(context.Background(), tt.req) + got, err := api.CreateFavoriteQuery(ctx, tt.req) require.Equal(t, tt.wantCode, status.Code(err)) if tt.wantCode != codes.OK { @@ -205,9 +258,9 @@ func TestCreateFavoriteQuery(t *testing.T) { } func TestDeleteFavoriteQuery(t *testing.T) { - var ( - queryID int64 = 1 - ) + userName := "unnamed" + var profileID int64 = 1 + var queryID int64 = 100 type mockArgs struct { req types.DeleteFavoriteQueryRequest @@ -222,9 +275,10 @@ func TestDeleteFavoriteQuery(t *testing.T) { wantCode codes.Code mockArgs *mockArgs + noUser bool }{ { - name: "ok", + name: "success", req: &userprofile.DeleteFavoriteQueryRequest{ Id: queryID, }, @@ -232,39 +286,57 @@ func TestDeleteFavoriteQuery(t *testing.T) { wantCode: codes.OK, mockArgs: &mockArgs{ req: types.DeleteFavoriteQueryRequest{ - ID: queryID, + ID: queryID, + ProfileID: profileID, }, }, }, { - name: "err_svc", + 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", req: &userprofile.DeleteFavoriteQueryRequest{ Id: queryID, }, wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.DeleteFavoriteQueryRequest{ - ID: queryID, + ID: queryID, + ProfileID: profileID, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newFavoriteQueriesTestData(t) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - DeleteFavoriteQuery(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.err). - Times(1) + 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) } - got, err := api.DeleteFavoriteQuery(context.Background(), tt.req) + got, err := api.DeleteFavoriteQuery(ctx, 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 8d79006..2860f1a 100644 --- a/internal/api/userprofile/v1/grpc/test_data.go +++ b/internal/api/userprofile/v1/grpc/test_data.go @@ -1,27 +1,18 @@ package grpc import ( - "context" - "errors" "testing" - "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") + "github.com/ozontech/seq-ui/internal/api/userprofile/v1/test" + repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository/mock" ) -func setupTestAPI(t *testing.T) (*API, *mock.MockService) { - ctrl := gomock.NewController(t) - mockedSvc := mock.NewMockService(ctrl) - return New(mockedSvc), mockedSvc +func newUserProfilesTestData(t *testing.T) (*API, *repo_mock.MockUserProfiles) { + mock, s, p := test.NewUserProfilesData(t) + return New(s, p), mock } -func withUser(userName string) context.Context { - return context.WithValue(context.Background(), types.UserKey{}, userName) +func newFavoriteQueriesTestData(t *testing.T) (*API, *repo_mock.MockFavoriteQueries) { + mock, s, p := test.NewFavoriteQueriesTestData(t) + return New(s, p), mock } diff --git a/internal/api/userprofile/v1/grpc/user_profiles.go b/internal/api/userprofile/v1/grpc/user_profiles.go index 550fd68..45371d2 100644 --- a/internal/api/userprofile/v1/grpc/user_profiles.go +++ b/internal/api/userprofile/v1/grpc/user_profiles.go @@ -21,13 +21,16 @@ 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 } @@ -57,7 +60,6 @@ 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 4913bb6..444db8b 100644 --- a/internal/api/userprofile/v1/grpc/user_profiles_test.go +++ b/internal/api/userprofile/v1/grpc/user_profiles_test.go @@ -1,6 +1,8 @@ package grpc import ( + "context" + "errors" "testing" "github.com/stretchr/testify/require" @@ -13,12 +15,10 @@ import ( ) func TestGetUserProfile(t *testing.T) { - var ( - userName = "unnamed" - timezone = "UTC" - onboardingVersion = `{"name1": "ver1", "name2": "ver2"}` - logColumns = []string{"val1", "val2"} - ) + userName := "unnamed" + timezone := "UTC" + onboardingVersion := `{"name1": "ver1", "name2": "ver2"}` + logColumns := []string{"val1", "val2"} type mockArgs struct { req types.GetOrCreateUserProfileRequest @@ -33,9 +33,10 @@ func TestGetUserProfile(t *testing.T) { wantCode codes.Code mockArgs *mockArgs + noUser bool }{ { - name: "ok", + name: "success", want: &userprofile.GetUserProfileResponse{ Timezone: timezone, OnboardingVersion: onboardingVersion, @@ -56,31 +57,38 @@ func TestGetUserProfile(t *testing.T) { }, }, { - name: "err_svc", + name: "err_no_user", + wantCode: codes.Unauthenticated, + noUser: true, + }, + { + name: "err_repo_random", wantCode: codes.Internal, mockArgs: &mockArgs{ req: types.GetOrCreateUserProfileRequest{ UserName: userName, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newUserProfilesTestData(t) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - GetOrCreateUserProfile(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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) } - ctx := withUser(userName) got, err := api.GetUserProfile(ctx, &userprofile.GetUserProfileRequest{}) require.Equal(t, tt.wantCode, status.Code(err)) @@ -94,12 +102,11 @@ func TestGetUserProfile(t *testing.T) { } func TestUpdateUserProfile(t *testing.T) { - var ( - userName = "unnamed" - validTimezone = "Europe/Moscow" - onboardingVersion = `{"name1": "ver1", "name2": "ver2"}` - logColumns = []string{"val1", "val2"} - ) + userName := "unnamed" + validTimezone := "Europe/Moscow" + invalidTimezone := "invalid timezone" + onboardingVersion := `{"name1": "ver1", "name2": "ver2"}` + logColumns := []string{"val1", "val2"} type mockArgs struct { req types.UpdateUserProfileRequest @@ -114,9 +121,10 @@ func TestUpdateUserProfile(t *testing.T) { wantCode codes.Code mockArgs *mockArgs + noUser bool }{ { - name: "ok", + name: "success_all", req: &userprofile.UpdateUserProfileRequest{ Timezone: &validTimezone, OnboardingVersion: &onboardingVersion, @@ -134,7 +142,94 @@ func TestUpdateUserProfile(t *testing.T) { }, }, { - name: "err_svc", + 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", req: &userprofile.UpdateUserProfileRequest{ Timezone: &validTimezone, }, @@ -144,25 +239,27 @@ func TestUpdateUserProfile(t *testing.T) { UserName: userName, Timezone: &validTimezone, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newUserProfilesTestData(t) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - UpdateUserProfile(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.err). - Times(1) + 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) } - 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 59377eb..b817660 100644 --- a/internal/api/userprofile/v1/http/api.go +++ b/internal/api/userprofile/v1/http/api.go @@ -3,16 +3,19 @@ package http import ( "github.com/go-chi/chi/v5" - "github.com/ozontech/seq-ui/internal/pkg/service/userprofile" + "github.com/ozontech/seq-ui/internal/api/profiles" + "github.com/ozontech/seq-ui/internal/pkg/service" ) type API struct { - service userprofile.Service + service service.Service + profiles *profiles.Profiles } -func New(svc userprofile.Service) *API { +func New(svc service.Service, p *profiles.Profiles) *API { return &API{ - service: svc, + service: svc, + profiles: p, } } diff --git a/internal/api/userprofile/v1/http/favorite_queries.go b/internal/api/userprofile/v1/http/favorite_queries.go index ba638dd..8a72eb7 100644 --- a/internal/api/userprofile/v1/http/favorite_queries.go +++ b/internal/api/userprofile/v1/http/favorite_queries.go @@ -29,9 +29,16 @@ func (a *API) serveGetFavoriteQueries(w http.ResponseWriter, r *http.Request) { wr := httputil.NewWriter(w) - req := types.GetFavoriteQueriesRequest{} - fqs, err := a.service.GetFavoriteQueries(ctx, req) + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + httputil.ProcessError(wr, err) + return + } + req := types.GetFavoriteQueriesRequest{ + ProfileID: profileID, + } + fqs, err := a.service.GetFavoriteQueries(ctx, req) if err != nil { httputil.ProcessError(wr, err) return @@ -78,20 +85,26 @@ func (a *API) serveCreateFavoriteQuery(w http.ResponseWriter, r *http.Request) { }, ) - req := types.GetOrCreateFavoriteQueryRequest{Query: httpReq.Query} + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + httputil.ProcessError(wr, err) + return + } + 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) @@ -129,8 +142,16 @@ func (a *API) serveDeleteFavoriteQuery(w http.ResponseWriter, r *http.Request) { Value: attribute.Int64Value(id), }) - req := types.DeleteFavoriteQueryRequest{ID: id} + profileID, err := a.profiles.GeIDFromContext(ctx) + if err != nil { + httputil.ProcessError(wr, err) + return + } + 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 ccb6189..a280567 100644 --- a/internal/api/userprofile/v1/http/favorite_queries_test.go +++ b/internal/api/userprofile/v1/http/favorite_queries_test.go @@ -1,11 +1,15 @@ package http import ( + "context" + "errors" "fmt" "net/http" - "strconv" + "net/http/httptest" + "strings" "testing" + "github.com/go-chi/chi/v5" "go.uber.org/mock/gomock" "github.com/ozontech/seq-ui/internal/api/httputil" @@ -13,9 +17,8 @@ import ( ) func TestServeGetFavoriteQueries(t *testing.T) { - var ( - relativeFrom = "300" - ) + userName := "unnamed" + var profileID int64 = 1 type mockArgs struct { req types.GetFavoriteQueriesRequest @@ -26,69 +29,105 @@ func TestServeGetFavoriteQueries(t *testing.T) { tests := []struct { name string - want getFavoriteQueriesResponse - wantErr bool + wantRespBody string + wantStatus int mockArgs *mockArgs + noUser bool }{ { - 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"}, - }, - }, + 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, + }, 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: 300}, - {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: 900, + }, + { + ID: 4, + Query: "test4", + }, }, }, }, { - name: "err_svc", - wantErr: true, + name: "err_no_user", + wantStatus: http.StatusUnauthorized, + noUser: true, + }, + { + name: "err_repo_random", + wantStatus: http.StatusInternalServerError, mockArgs: &mockArgs{ - err: errSomethingWrong, + req: types.GetFavoriteQueriesRequest{ + ProfileID: profileID, + }, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newFavoriteQueriesTestData(t) + req := httptest.NewRequest(http.MethodGet, "/userprofile/v1/queries/favorite", http.NoBody) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - GetFavoriteQueries(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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) } - httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[struct{}, getFavoriteQueriesResponse]{ - Method: http.MethodGet, - Target: "/userprofile/v1/queries/favorite", - Handler: api.serveGetFavoriteQueries, - Want: tt.want, - WantErr: tt.wantErr, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveGetFavoriteQueries, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } } func TestServeCreateFavoriteQuery(t *testing.T) { - var ( - relativeFrom = "300" - query = "test" - queryName = "my query" - ) + 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() + } type mockArgs struct { req types.GetOrCreateFavoriteQueryRequest @@ -99,66 +138,107 @@ func TestServeCreateFavoriteQuery(t *testing.T) { tests := []struct { name string - req createFavoriteQueryRequest - want createFavoriteQueryResponse - wantErr bool + reqBody string + wantRespBody string + wantStatus int mockArgs *mockArgs + noUser bool }{ { - name: "ok", - req: createFavoriteQueryRequest{Query: query, Name: &queryName, RelativeFrom: &relativeFrom}, - want: createFavoriteQueryResponse{ID: "1"}, + name: "success", + reqBody: formatReqBody(query, "my query", "300"), + wantRespBody: fmt.Sprintf(`{"id":"%d"}`, queryID), + wantStatus: http.StatusOK, mockArgs: &mockArgs{ req: types.GetOrCreateFavoriteQueryRequest{ + ProfileID: profileID, Query: query, Name: "my query", RelativeFrom: 300, }, - resp: 1, + resp: queryID, }, }, { - name: "err_svc", - req: createFavoriteQueryRequest{Query: query, Name: &queryName, RelativeFrom: &relativeFrom}, - wantErr: true, + name: "success_only_query", + reqBody: formatReqBody(query, "", ""), + wantRespBody: fmt.Sprintf(`{"id":"%d"}`, queryID), + wantStatus: http.StatusOK, mockArgs: &mockArgs{ req: types.GetOrCreateFavoriteQueryRequest{ - Query: query, - Name: "my query", - RelativeFrom: 300, + ProfileID: profileID, + Query: query, }, - err: errSomethingWrong, + 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, + }, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newFavoriteQueriesTestData(t) + req := httptest.NewRequest(http.MethodPost, "/userprofile/v1/queries/favorite", strings.NewReader(tt.reqBody)) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - GetOrCreateFavoriteQuery(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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) } - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveCreateFavoriteQuery, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } } func TestServeDeleteFavoriteQuery(t *testing.T) { + userName := "unnamed" + var profileID int64 = 1 + type mockArgs struct { req types.DeleteFavoriteQueryRequest err error @@ -167,52 +247,77 @@ func TestServeDeleteFavoriteQuery(t *testing.T) { tests := []struct { name string - wantErr bool + id string + wantStatus int mockArgs *mockArgs + noUser bool }{ { - name: "ok", + name: "success", + id: "100", + wantStatus: http.StatusOK, mockArgs: &mockArgs{ req: types.DeleteFavoriteQueryRequest{ - ID: 100, + ID: 100, + ProfileID: profileID, }, }, }, { - name: "err_svc", - wantErr: true, + 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, mockArgs: &mockArgs{ req: types.DeleteFavoriteQueryRequest{ - ID: 100, + ID: 100, + ProfileID: profileID, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + 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)) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - DeleteFavoriteQuery(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.err). - Times(1) + 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) } - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveDeleteFavoriteQuery, + WantStatus: tt.wantStatus, }) }) } diff --git a/internal/api/userprofile/v1/http/test_data.go b/internal/api/userprofile/v1/http/test_data.go index 71b8e99..fcfb67a 100644 --- a/internal/api/userprofile/v1/http/test_data.go +++ b/internal/api/userprofile/v1/http/test_data.go @@ -1,41 +1,18 @@ package http import ( - "context" - "errors" - "net/http" "testing" - "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") + "github.com/ozontech/seq-ui/internal/api/userprofile/v1/test" + repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository/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 newUserProfilesTestData(t *testing.T) (*API, *repo_mock.MockUserProfiles) { + mock, s, p := test.NewUserProfilesData(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) - } +func newFavoriteQueriesTestData(t *testing.T) (*API, *repo_mock.MockFavoriteQueries) { + mock, s, p := test.NewFavoriteQueriesTestData(t) + return New(s, p), mock } diff --git a/internal/api/userprofile/v1/http/user_profiles.go b/internal/api/userprofile/v1/http/user_profiles.go index e116c67..e008f0b 100644 --- a/internal/api/userprofile/v1/http/user_profiles.go +++ b/internal/api/userprofile/v1/http/user_profiles.go @@ -35,13 +35,14 @@ 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)) } @@ -92,7 +93,6 @@ 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 2f41d09..2cdfd30 100644 --- a/internal/api/userprofile/v1/http/user_profiles_test.go +++ b/internal/api/userprofile/v1/http/user_profiles_test.go @@ -1,7 +1,13 @@ package http import ( + "context" + "encoding/json" + "errors" + "fmt" "net/http" + "net/http/httptest" + "strings" "testing" "go.uber.org/mock/gomock" @@ -11,12 +17,10 @@ import ( ) func TestServeGetUserProfile(t *testing.T) { - var ( - userName = "unnamed" - timezone = "UTC" - onboardingVersion = `{"name1": "ver1", "name2": "ver2"}` - logColumns = []string{"val1", "val2"} - ) + userName := "unnamed" + timezone := "UTC" + onboardingVersion := `{"name1": "ver1", "name2": "ver2"}` + logColumns := []string{"val1", "val2"} type mockArgs struct { req types.GetOrCreateUserProfileRequest @@ -27,18 +31,16 @@ func TestServeGetUserProfile(t *testing.T) { tests := []struct { name string - want userProfile - wantErr bool + wantRespBody string + wantStatus int mockArgs *mockArgs + noUser bool }{ { - name: "ok", - want: userProfile{ - Timezone: timezone, - OnboardingVersion: onboardingVersion, - LogColumns: logColumns, - }, + name: "success", + wantRespBody: `{"timezone":"UTC","onboardingVersion":"{\"name1\": \"ver1\", \"name2\": \"ver2\"}","log_columns":["val1","val2"]}`, + wantStatus: http.StatusOK, mockArgs: &mockArgs{ req: types.GetOrCreateUserProfileRequest{ UserName: userName, @@ -53,48 +55,76 @@ func TestServeGetUserProfile(t *testing.T) { }, }, { - name: "err_svc", - wantErr: true, + name: "err_no_user", + wantStatus: http.StatusUnauthorized, + noUser: true, + }, + { + name: "err_repo_random", + wantStatus: http.StatusInternalServerError, mockArgs: &mockArgs{ req: types.GetOrCreateUserProfileRequest{ UserName: userName, }, - err: errSomethingWrong, + err: errors.New("random repo err"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newUserProfilesTestData(t) + req := httptest.NewRequest(http.MethodGet, "/userprofile/v1/profile", http.NoBody) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - GetOrCreateUserProfile(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err). - Times(1) + 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)) } - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveGetUserProfile, + WantRespBody: tt.wantRespBody, + WantStatus: tt.wantStatus, }) }) } } func TestServeUpdateUserProfile(t *testing.T) { - var ( - userName = "unnamed" - validTimezone = "Europe/Moscow" - onboardingVersion = `{"name1": "ver1", "name2": "ver2"}` - logColumns = []string{"val1", "val2"} - ) + 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() + } type mockArgs struct { req types.UpdateUserProfileRequest @@ -104,20 +134,16 @@ func TestServeUpdateUserProfile(t *testing.T) { tests := []struct { name string - req updateUserProfileRequest - wantErr bool + reqBody string + wantStatus int mockArgs *mockArgs + noUser bool }{ { - name: "ok", - req: updateUserProfileRequest{ - Timezone: &validTimezone, - OnboardingVersion: &onboardingVersion, - LogColumns: &struct { - Columns []string "json:\"columns\"" - }{Columns: logColumns}, - }, + name: "success_all", + reqBody: formatReqBody(validTimezone, onboardingVersion, logColumns), + wantStatus: http.StatusOK, mockArgs: &mockArgs{ req: types.UpdateUserProfileRequest{ UserName: userName, @@ -128,47 +154,116 @@ func TestServeUpdateUserProfile(t *testing.T) { }, }, { - name: "err_svc", - req: updateUserProfileRequest{ - Timezone: &validTimezone, - OnboardingVersion: &onboardingVersion, - LogColumns: &struct { - Columns []string "json:\"columns\"" - }{Columns: logColumns}, + name: "success_only_timezone", + reqBody: formatReqBody(validTimezone, "", nil), + wantStatus: http.StatusOK, + mockArgs: &mockArgs{ + req: types.UpdateUserProfileRequest{ + UserName: userName, + Timezone: &validTimezone, + }, }, - wantErr: true, + }, + { + name: "success_only_onboarding_ver", + reqBody: formatReqBody("", onboardingVersion, nil), + wantStatus: http.StatusOK, mockArgs: &mockArgs{ req: types.UpdateUserProfileRequest{ UserName: userName, - Timezone: &validTimezone, OnboardingVersion: &onboardingVersion, - LogColumns: &types.LogColumns{LogColumns: logColumns}, }, - err: errSomethingWrong, + }, + }, + { + 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"), }, }, } - for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, mockedSvc := setupTestAPI(t) + api, mockedRepo := newUserProfilesTestData(t) + req := httptest.NewRequest(http.MethodPatch, "/userprofile/v1/profile", strings.NewReader(tt.reqBody)) if tt.mockArgs != nil { - mockedSvc.EXPECT(). - UpdateUserProfile(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.err). - Times(1) + 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)) } - 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, + httputil.DoTestHTTP(t, httputil.TestDataHTTP{ + Req: req, + Handler: api.serveUpdateUserProfile, + WantStatus: tt.wantStatus, }) }) } diff --git a/internal/api/userprofile/v1/test/data.go b/internal/api/userprofile/v1/test/data.go new file mode 100644 index 0000000..9df8edc --- /dev/null +++ b/internal/api/userprofile/v1/test/data.go @@ -0,0 +1,34 @@ +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 c74a46a..66f5320 100644 --- a/internal/api/userprofile/v1/userprofile.go +++ b/internal/api/userprofile/v1/userprofile.go @@ -3,9 +3,10 @@ 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/userprofile" + "github.com/ozontech/seq-ui/internal/pkg/service" ) type UserProfile struct { @@ -13,10 +14,10 @@ type UserProfile struct { httpAPI *http_api.API } -func New(svc userprofile.Service) *UserProfile { +func New(svc service.Service, p *profiles.Profiles) *UserProfile { return &UserProfile{ - grpcAPI: grpc_api.New(svc), - httpAPI: http_api.New(svc), + grpcAPI: grpc_api.New(svc, p), + httpAPI: http_api.New(svc, p), } } diff --git a/internal/app/auth/oidc.go b/internal/app/auth/oidc.go index 15caf35..4b9fdbd 100644 --- a/internal/app/auth/oidc.go +++ b/internal/app/auth/oidc.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "net/http" - "slices" "time" "github.com/coreos/go-oidc/v3/oidc" @@ -170,8 +169,10 @@ func (p *oidcProvider) checkClients(clients []string) error { } for _, client := range clients { - if slices.Contains(p.allowedClients, client) { - return nil + for _, allowedClient := range p.allowedClients { + if client == allowedClient { + return nil + } } } diff --git a/internal/pkg/service/async_searches/mock/service.go b/internal/pkg/service/async_searches/mock/service.go deleted file mode 100644 index 4349018..0000000 --- a/internal/pkg/service/async_searches/mock/service.go +++ /dev/null @@ -1,117 +0,0 @@ -// 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 d6f4825..0960785 100644 --- a/internal/pkg/service/async_searches/service.go +++ b/internal/pkg/service/async_searches/service.go @@ -14,7 +14,6 @@ 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" @@ -27,23 +26,20 @@ const ( deleteExpiredAsyncSearchesInterval = 1 * time.Minute ) -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 { +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, @@ -54,12 +50,11 @@ func New(ctx context.Context, repo repository.AsyncSearches, seqDB seqdb.Client, return s } -func (s *service) StartAsyncSearch(ctx context.Context, req *seqapi.StartAsyncSearchRequest) (*seqapi.StartAsyncSearchResponse, error) { - ownerID, err := profiles.GetIDFromContext(ctx) - if err != nil { - return nil, err - } - +func (s *Service) StartAsyncSearch( + ctx context.Context, + ownerID int64, + req *seqapi.StartAsyncSearchRequest, +) (*seqapi.StartAsyncSearchResponse, error) { if utf8.RuneCountInString(req.Query) > s.cfg.ListQueryLengthLimit { metric.AsyncSearchQueryTooLong.Inc() } @@ -87,12 +82,11 @@ func (s *service) StartAsyncSearch(ctx context.Context, req *seqapi.StartAsyncSe return resp, nil } -func (s *service) DeleteAsyncSearch(ctx context.Context, req *seqapi.DeleteAsyncSearchRequest) (*seqapi.DeleteAsyncSearchResponse, error) { - ownerID, err := profiles.GetIDFromContext(ctx) - if err != nil { - return nil, err - } - +func (s *Service) DeleteAsyncSearch( + ctx context.Context, + ownerID int64, + req *seqapi.DeleteAsyncSearchRequest, +) (*seqapi.DeleteAsyncSearchResponse, 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) @@ -121,12 +115,11 @@ func (s *service) DeleteAsyncSearch(ctx context.Context, req *seqapi.DeleteAsync return resp, nil } -func (s *service) CancelAsyncSearch(ctx context.Context, req *seqapi.CancelAsyncSearchRequest) (*seqapi.CancelAsyncSearchResponse, error) { - ownerID, err := profiles.GetIDFromContext(ctx) - if err != nil { - return nil, err - } - +func (s *Service) CancelAsyncSearch( + ctx context.Context, + ownerID int64, + req *seqapi.CancelAsyncSearchRequest, +) (*seqapi.CancelAsyncSearchResponse, 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) @@ -144,7 +137,10 @@ func (s *service) CancelAsyncSearch(ctx context.Context, req *seqapi.CancelAsync 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) @@ -160,7 +156,10 @@ func (s *service) FetchAsyncSearchResult(ctx context.Context, req *seqapi.FetchA 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, }) @@ -192,7 +191,7 @@ func (s *service) GetAsyncSearchesList(ctx context.Context, req *seqapi.GetAsync 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 @@ -201,7 +200,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() @@ -218,7 +217,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/service.go b/internal/pkg/service/dashboards.go similarity index 50% rename from internal/pkg/service/dashboards/service.go rename to internal/pkg/service/dashboards.go index c84bd69..e87b391 100644 --- a/internal/pkg/service/dashboards/service.go +++ b/internal/pkg/service/dashboards.go @@ -1,4 +1,4 @@ -package dashboards +package service import ( "context" @@ -6,77 +6,37 @@ 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" ) -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, - } -} - +// GetAllDashboards from underlying repository. 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.GetAll(ctx, req) + return s.repo.Dashboards.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.GetMy(ctx, req) + return s.repo.Dashboards.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.GetByUUID(ctx, id) + return s.repo.Dashboards.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'") } @@ -84,16 +44,11 @@ func (s *service) CreateDashboard(ctx context.Context, req types.CreateDashboard return "", types.NewErrInvalidRequestField("empty 'meta'") } - return s.repo.Create(ctx, req) + return s.repo.Dashboards.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 } @@ -101,34 +56,24 @@ func (s *service) UpdateDashboard(ctx context.Context, req types.UpdateDashboard return types.ErrEmptyUpdateRequest } - return s.repo.Update(ctx, req) + return s.repo.Dashboards.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.Delete(ctx, req) + return s.repo.Dashboards.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.Search(ctx, req) + return s.repo.Dashboards.Search(ctx, req) } func checkUUID(v string) error { diff --git a/internal/pkg/service/dashboards/mock/service.go b/internal/pkg/service/dashboards/mock/service.go deleted file mode 100644 index 31e43b8..0000000 --- a/internal/pkg/service/dashboards/mock/service.go +++ /dev/null @@ -1,145 +0,0 @@ -// 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/favorite_queries.go b/internal/pkg/service/favorite_queries.go new file mode 100644 index 0000000..9da4b5e --- /dev/null +++ b/internal/pkg/service/favorite_queries.go @@ -0,0 +1,30 @@ +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 deleted file mode 100644 index 02928a0..0000000 --- a/internal/pkg/service/massexport/mock/service.go +++ /dev/null @@ -1,115 +0,0 @@ -// 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/pkg/service/service.go b/internal/pkg/service/service.go new file mode 100644 index 0000000..4a34733 --- /dev/null +++ b/internal/pkg/service/service.go @@ -0,0 +1,35 @@ +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 new file mode 100644 index 0000000..e36685d --- /dev/null +++ b/internal/pkg/service/user_profiles.go @@ -0,0 +1,27 @@ +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 deleted file mode 100644 index bc3ff80..0000000 --- a/internal/pkg/service/userprofile/mock/service.go +++ /dev/null @@ -1,115 +0,0 @@ -// 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 deleted file mode 100644 index a4672cd..0000000 --- a/internal/pkg/service/userprofile/service.go +++ /dev/null @@ -1,92 +0,0 @@ -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) -}