Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/implement pagination for regions #449

Merged
merged 3 commits into from
Mar 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/server/http/entities/region.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ type RegionResponse struct {
} // @Name Region

type RegionListResponse struct {
Regions []*RegionResponse `json:"regions"`
Data []*RegionResponse `json:"data"`
Pagination *Pagination `json:"pagination,omitempty" validate:"optional"`
} // @Name RegionList
7 changes: 4 additions & 3 deletions internal/server/http/handler/v1/region/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/green-ecolution/green-ecolution-backend/internal/server/http/handler/v1/errorhandler"
"github.com/green-ecolution/green-ecolution-backend/internal/service"
"github.com/green-ecolution/green-ecolution-backend/internal/utils"
"github.com/green-ecolution/green-ecolution-backend/internal/utils/pagination"
)

// @Summary Get all regions
Expand All @@ -25,7 +26,7 @@ import (
func GetAllRegions(svc service.RegionService) fiber.Handler {
return func(c *fiber.Ctx) error {
ctx := c.Context()
r, err := svc.GetAll(ctx)
r, totalCount, err := svc.GetAll(ctx)
if err != nil {
return errorhandler.HandleError(err)
}
Expand All @@ -38,8 +39,8 @@ func GetAllRegions(svc service.RegionService) fiber.Handler {
})

return c.JSON(entities.RegionListResponse{
Regions: dto,
Pagination: nil, // TODO: Handle pagination
Data: dto,
Pagination: pagination.Create(ctx, totalCount),
})
}
}
Expand Down
109 changes: 100 additions & 9 deletions internal/server/http/handler/v1/region/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/green-ecolution/green-ecolution-backend/internal/entities"
serverEntities "github.com/green-ecolution/green-ecolution-backend/internal/server/http/entities"
"github.com/green-ecolution/green-ecolution-backend/internal/server/http/handler/v1/region"
"github.com/green-ecolution/green-ecolution-backend/internal/server/http/middleware"
"github.com/green-ecolution/green-ecolution-backend/internal/service"
serviceMock "github.com/green-ecolution/green-ecolution-backend/internal/service/_mock"
"github.com/green-ecolution/green-ecolution-backend/internal/utils"
Expand All @@ -18,9 +19,10 @@ import (
)

func TestGetAllRegions(t *testing.T) {
t.Run("should return all regions successfully", func(t *testing.T) {
mockRegionService := serviceMock.NewMockRegionService(t)
t.Run("should return all regions successfully with default pagination values", func(t *testing.T) {
app := fiber.New()
app.Use(middleware.PaginationMiddleware())
mockRegionService := serviceMock.NewMockRegionService(t)
handler := region.GetAllRegions(mockRegionService)

expectedRegions := []*entities.Region{
Expand All @@ -30,7 +32,7 @@ func TestGetAllRegions(t *testing.T) {

mockRegionService.EXPECT().GetAll(
mock.Anything,
).Return(expectedRegions, nil)
).Return(expectedRegions, int64(len(expectedRegions)), nil)

app.Get("/v1/region", handler)

Expand All @@ -46,21 +48,104 @@ func TestGetAllRegions(t *testing.T) {
var response serverEntities.RegionListResponse
err = utils.ParseJSONResponse(resp, &response)
assert.NoError(t, err)
assert.Len(t, response.Regions, 2)
assert.Equal(t, "Region A", response.Regions[0].Name)
assert.Equal(t, "Region B", response.Regions[1].Name)

// assert data
assert.Len(t, response.Data, 2)
assert.Equal(t, "Region A", response.Data[0].Name)
assert.Equal(t, "Region B", response.Data[1].Name)

// assert pagination
assert.Empty(t, response.Pagination)

mockRegionService.AssertExpectations(t)
})

t.Run("should return all regions successfully with limit 1 and offset 1", func(t *testing.T) {
app := fiber.New()
app.Use(middleware.PaginationMiddleware())
mockRegionService := serviceMock.NewMockRegionService(t)
handler := region.GetAllRegions(mockRegionService)

expectedRegions := []*entities.Region{
{ID: 2, Name: "Region B"},
}

mockRegionService.EXPECT().GetAll(
mock.Anything,
).Return(expectedRegions, int64(len(expectedRegions)), nil)

app.Get("/v1/region", handler)

// when
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/v1/region?page=2&limit=1", nil)
resp, err := app.Test(req, -1)
defer resp.Body.Close()

// then
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)

var response serverEntities.RegionListResponse
err = utils.ParseJSONResponse(resp, &response)
assert.NoError(t, err)

// assert data
assert.Len(t, response.Data, 1)
assert.Equal(t, "Region B", response.Data[0].Name)

// assert pagination
assert.Equal(t, int32(2), response.Pagination.CurrentPage)
assert.Equal(t, int64(1), response.Pagination.Total)
assert.Nil(t, response.Pagination.NextPage)
assert.Equal(t, int32(1), *response.Pagination.PrevPage)
assert.Equal(t, int32((len(expectedRegions))/1), response.Pagination.TotalPages)

mockRegionService.AssertExpectations(t)
})

t.Run("should return error when page is invalid", func(t *testing.T) {
app := fiber.New()
app.Use(middleware.PaginationMiddleware())
mockRegionService := serviceMock.NewMockRegionService(t)
handler := region.GetAllRegions(mockRegionService)
app.Get("/v1/region", handler)

req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/v1/region?page=0&limit=1", nil)
resp, err := app.Test(req, -1)
defer resp.Body.Close()

assert.Nil(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

mockRegionService.AssertExpectations(t)
})

t.Run("should return error when limit is invalid", func(t *testing.T) {
app := fiber.New()
app.Use(middleware.PaginationMiddleware())
mockRegionService := serviceMock.NewMockRegionService(t)
handler := region.GetAllRegions(mockRegionService)
app.Get("/v1/region", handler)

req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/v1/region?page=1&limit=0", nil)
resp, err := app.Test(req, -1)
defer resp.Body.Close()

assert.Nil(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

mockRegionService.AssertExpectations(t)
})

t.Run("should return empty region list when no regions found", func(t *testing.T) {
mockRegionService := serviceMock.NewMockRegionService(t)
app := fiber.New()
app.Use(middleware.PaginationMiddleware())
handler := region.GetAllRegions(mockRegionService)

mockRegionService.EXPECT().GetAll(
mock.Anything,
).Return([]*entities.Region{}, nil)
).Return([]*entities.Region{}, int64(0), nil)

app.Get("/v1/region", handler)

Expand All @@ -76,19 +161,25 @@ func TestGetAllRegions(t *testing.T) {
var response serverEntities.RegionListResponse
err = utils.ParseJSONResponse(resp, &response)
assert.NoError(t, err)
assert.Len(t, response.Regions, 0)

// assert data
assert.Len(t, response.Data, 0)

// assert pagination
assert.Empty(t, response.Pagination)

mockRegionService.AssertExpectations(t)
})

t.Run("should return 500 when service returns an error", func(t *testing.T) {
mockRegionService := serviceMock.NewMockRegionService(t)
app := fiber.New()
app.Use(middleware.PaginationMiddleware())
handler := region.GetAllRegions(mockRegionService)

mockRegionService.EXPECT().GetAll(
mock.Anything,
).Return(nil, errors.New("service error"))
).Return(nil, int64(0), errors.New("service error"))

app.Get("/v1/region", handler)

Expand Down
9 changes: 7 additions & 2 deletions internal/server/http/handler/v1/region/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/gofiber/fiber/v2"
"github.com/green-ecolution/green-ecolution-backend/internal/entities"
"github.com/green-ecolution/green-ecolution-backend/internal/server/http/middleware"
serviceMock "github.com/green-ecolution/green-ecolution-backend/internal/service/_mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
Expand All @@ -17,19 +18,23 @@ func TestRegisterRoutes(t *testing.T) {
t.Run("should call GET handler", func(t *testing.T) {
mockRegionService := serviceMock.NewMockRegionService(t)
app := fiber.New()
app.Use(middleware.PaginationMiddleware())
RegisterRoutes(app, mockRegionService)

ctx := context.WithValue(context.Background(), "page", int32(1))
ctx = context.WithValue(ctx, "limit", int32(-1))

expectedRegions := []*entities.Region{
{ID: 1, Name: "Region A"},
{ID: 2, Name: "Region B"},
}

mockRegionService.EXPECT().GetAll(
mock.Anything,
).Return(expectedRegions, nil)
).Return(expectedRegions, int64(len(expectedRegions)), nil)

// when
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/", nil)

// then
resp, err := app.Test(req)
Expand Down
8 changes: 4 additions & 4 deletions internal/service/domain/region/region.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ func NewRegionService(regionRepository storage.RegionRepository) service.RegionS
}
}

func (s *RegionService) GetAll(ctx context.Context) ([]*domain.Region, error) {
func (s *RegionService) GetAll(ctx context.Context) ([]*domain.Region, int64, error) {
log := logger.GetLogger(ctx)
regions, err := s.regionRepo.GetAll(ctx)
regions, totalCount, err := s.regionRepo.GetAll(ctx)
if err != nil {
log.Debug("failed to get region by id", "error", err)
return nil, service.MapError(ctx, err, service.ErrorLogEntityNotFound)
return nil, 0, service.MapError(ctx, err, service.ErrorLogEntityNotFound)
}

return regions, nil
return regions, totalCount, nil
}

func (s *RegionService) GetByID(ctx context.Context, id int32) (*domain.Region, error) {
Expand Down
10 changes: 6 additions & 4 deletions internal/service/domain/region/region_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ func TestRegionService_GetAll(t *testing.T) {
}

// when
repo.EXPECT().GetAll(rootCtx).Return(expectedRegions, nil)
regions, err := svc.GetAll(rootCtx)
repo.EXPECT().GetAll(rootCtx).Return(expectedRegions, int64(len(expectedRegions)), nil)
regions, totalCount, err := svc.GetAll(rootCtx)

// then
assert.NoError(t, err)
assert.Equal(t, expectedRegions, regions)
assert.Equal(t, int64(len(expectedRegions)), totalCount)
})

t.Run("should return error when repository fails", func(t *testing.T) {
Expand All @@ -40,12 +41,13 @@ func TestRegionService_GetAll(t *testing.T) {
svc := NewRegionService(repo)
expectedErr := errors.New("GetAll failed")

repo.EXPECT().GetAll(rootCtx).Return(nil, expectedErr)
regions, err := svc.GetAll(rootCtx)
repo.EXPECT().GetAll(rootCtx).Return(nil, int64(0), expectedErr)
regions, totalCount, err := svc.GetAll(rootCtx)

// then
assert.Error(t, err)
assert.Nil(t, regions)
assert.Equal(t, int64(0), totalCount)
//assert.EqualError(t, err, "500: GetAll failed")
})
}
Expand Down
2 changes: 1 addition & 1 deletion internal/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ type AuthService interface {

type RegionService interface {
Service
GetAll(ctx context.Context) ([]*domain.Region, error)
GetAll(ctx context.Context) ([]*domain.Region, int64, error)
GetByID(ctx context.Context, id int32) (*domain.Region, error)
}

Expand Down
7 changes: 6 additions & 1 deletion internal/storage/postgres/queries/region.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
-- name: GetAllRegions :many
SELECT * FROM regions ORDER BY id;
SELECT * FROM regions
ORDER BY id
LIMIT $1 OFFSET $2;

-- name: GetAllRegionsCount :one
SELECT COUNT(*) FROM regions;

-- name: GetRegionById :one
SELECT * FROM regions WHERE id = $1;
Expand Down
44 changes: 29 additions & 15 deletions internal/storage/postgres/region/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,50 @@ import (
"github.com/green-ecolution/green-ecolution-backend/internal/entities"
"github.com/green-ecolution/green-ecolution-backend/internal/logger"
sqlc "github.com/green-ecolution/green-ecolution-backend/internal/storage/postgres/_sqlc"
"github.com/green-ecolution/green-ecolution-backend/internal/utils/pagination"
"github.com/jackc/pgx/v5"
)

func (r *RegionRepository) GetAll(ctx context.Context) ([]*entities.Region, error) {
func (r *RegionRepository) GetAll(ctx context.Context) ([]*entities.Region, int64, error) {
log := logger.GetLogger(ctx)
rows, err := r.store.GetAllRegions(ctx)
page, limit, err := pagination.GetValues(ctx)
if err != nil {
log.Debug("failed to get regions in db", "error", err)
return nil, r.store.MapError(err, sqlc.Region{})
return nil, 0, r.store.MapError(err, sqlc.Region{})
}

return r.mapper.FromSqlList(rows), nil
}
totalCount, err := r.store.GetAllRegionsCount(ctx)
if err != nil {
log.Debug("failed to get total regions count in db", "error", err)
return nil, 0, r.store.MapError(err, sqlc.Region{})
}

if totalCount == 0 {
return []*entities.Region{}, 0, nil
}

if limit == -1 {
limit = int32(totalCount)
page = 1
}

rows, err := r.store.GetAllRegions(ctx, &sqlc.GetAllRegionsParams{
Limit: limit,
Offset: (page - 1) * limit,
})

func (r *RegionRepository) GetByID(ctx context.Context, id int32) (*entities.Region, error) {
log := logger.GetLogger(ctx)
row, err := r.store.GetRegionById(ctx, id)
if err != nil {
log.Debug("failed to get region by id", "error", err, "region_id", id)
return nil, r.store.MapError(err, sqlc.Region{})
log.Debug("failed to get regions in db", "error", err)
return nil, 0, r.store.MapError(err, sqlc.Region{})
}

return r.mapper.FromSql(row), nil
return r.mapper.FromSqlList(rows), totalCount, nil
}

func (r *RegionRepository) GetByName(ctx context.Context, name string) (*entities.Region, error) {
func (r *RegionRepository) GetByID(ctx context.Context, id int32) (*entities.Region, error) {
log := logger.GetLogger(ctx)
row, err := r.store.GetRegionByName(ctx, name)
row, err := r.store.GetRegionById(ctx, id)
if err != nil {
log.Debug("failed to get region by name", "region_name", name, "error", err)
log.Debug("failed to get region by id", "error", err, "region_id", id)
return nil, r.store.MapError(err, sqlc.Region{})
}

Expand Down
Loading