Skip to content

Commit

Permalink
✅test: add end-to-end test case (#16)
Browse files Browse the repository at this point in the history
* ✨ feat: initialize test module

* 🔧 fix: update Golang Fiber config for test environment

* 🚧 test(province): add sample test case

* 💡 chore: add important note about testing

* ✅ test(province): add test case

* 🚑 fix: correct reflection for pointer usage

* ♻️ 🥅 refactor: improve error handling and type checking

* ✅ test(province): add test case with cities

* ✅ test(city): add test case

* 🏷️ chore: update entity to use base entity type

* 🐛 fix: return 404 when data is empty

* 🌱 chore: add CreateDistricts seeder for testing

* ✅ test(district): add test cases for parent relations

* 🩹 feat: add missing cleanup functionality

* ✅ test(district): add test case

* ✅ test(province): add test case with nested relations

* 🌱 feat(village): add seeder and cleanup function for testing

* ✅ test(village): add test case

* ✅ test(village): add test case for relations

* ✨ test: add sample database for testing
  • Loading branch information
aikuci authored Sep 7, 2024
1 parent 4bdbac4 commit 4b66723
Show file tree
Hide file tree
Showing 20 changed files with 959 additions and 75 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*.db
*.sqlite
*.sqlite3
!*test.sqlite3

# Env Files
.env
Expand Down
4 changes: 2 additions & 2 deletions example-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ web:
log:
level: 6
database:
dialect: pg
# dsn: postgresql://postgres:postgres@localhost:5432
dialect: sqlite
dsn: db/test.sqlite3
pool:
idle: 6
max: 100
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/google/uuid v1.6.0
github.com/lib/pq v1.10.9
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
go.uber.org/zap v1.27.0
gorm.io/driver/postgres v1.5.9
gorm.io/driver/sqlite v1.5.6
Expand All @@ -18,6 +19,7 @@ require (

require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
Expand All @@ -39,6 +41,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
Expand Down
34 changes: 24 additions & 10 deletions internal/config/fiber.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,21 @@ func NewErrorHandler(viper *viper.Viper) fiber.ErrorHandler {
}

func newLimiterConfig(viper *viper.Viper) fiber.Handler {
storage := sqlite3.New(sqlite3.Config{
Database: "./storage/log/fiber-limiter.sqlite3",
Table: "fiber_storage",
Reset: false,
GCInterval: 10 * time.Second,
MaxOpenConns: 100,
MaxIdleConns: 100,
ConnMaxLifetime: 1 * time.Second,
})

limiterConfig := func() limiter.Config {
if viper.GetString("app.mode") == "test" {
return newTestLimiterConfig()
}

storage := sqlite3.New(sqlite3.Config{
Database: "./storage/log/fiber-limiter.sqlite3",
Table: "fiber_storage",
Reset: false,
GCInterval: 10 * time.Second,
MaxOpenConns: 100,
MaxIdleConns: 100,
ConnMaxLifetime: 1 * time.Second,
})

if viper.GetString("app.mode") == "production" {
return newProductionLimiterConfig(storage)
}
Expand All @@ -97,6 +101,16 @@ func newLimiterConfig(viper *viper.Viper) fiber.Handler {
return limiter.New(limiterConfig)
}

func newTestLimiterConfig() limiter.Config {
// Ignoring storage for Test Environment
// When running tests, the current working directory might differ from your expected path.
return limiter.Config{
Next: func(c *fiber.Ctx) bool {
return true
},
}
}

func newDevelopmentLimiterConfig(storage fiber.Storage) limiter.Config {
return limiter.Config{
Next: func(c *fiber.Ctx) bool {
Expand Down
2 changes: 1 addition & 1 deletion internal/entity/city_entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package entity
import "github.com/lib/pq"

type City struct {
ID int `gorm:"primaryKey;autoIncrement:false"`
Base
ProvinceID int `gorm:"column:id_province;primaryKey;autoIncrement:false"`
Code string `gorm:"column:code;size:18"`
Name string `gorm:"column:name"`
Expand Down
2 changes: 1 addition & 1 deletion internal/entity/district_entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package entity
import "github.com/lib/pq"

type District struct {
ID int `gorm:"primaryKey;autoIncrement:false"`
Base
CityID int `gorm:"column:id_city;primaryKey;autoIncrement:false"`
ProvinceID int `gorm:"column:id_province;primaryKey;autoIncrement:false"`
Code string `gorm:"column:code;size:18"`
Expand Down
2 changes: 1 addition & 1 deletion internal/entity/village_entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package entity
import "github.com/lib/pq"

type Village struct {
ID int `gorm:"primaryKey;autoIncrement:false"`
Base
DistrictID int `gorm:"column:id_district;primaryKey;autoIncrement:false"`
CityID int `gorm:"column:id_city;primaryKey;autoIncrement:false"`
ProvinceID int `gorm:"column:id_province;primaryKey;autoIncrement:false"`
Expand Down
20 changes: 16 additions & 4 deletions internal/usecase/city_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,27 @@ func (uc *City) GetById(ctx context.Context, request model.GetCityByIDRequest[[]
return appusecase.Wrapper[entity.City](
appusecase.NewContext(ctx, uc.Log, uc.DB, request),
func(ctx *appusecase.Context[model.GetCityByIDRequest[[]int]]) (*[]entity.City, int64, error) {
id := ctx.Request.ID
idProvince := ctx.Request.IDProvince

where := map[string]interface{}{}
if ctx.Request.ID != nil {
where["id"] = ctx.Request.ID
if id != nil {
where["id"] = id
}
if ctx.Request.IDProvince != nil {
where["id_province"] = ctx.Request.IDProvince
if idProvince != nil {
where["id_province"] = idProvince
}

collections, total, err := uc.Repository.FindAndCountBy(ctx.DB, where)

if len(collections) == 0 {
errorMessage := fmt.Sprintf("failed to get cities data with ID: %d", id)
if idProvince != nil {
errorMessage += fmt.Sprintf(" and ID Province: %d", idProvince)
}
return nil, 0, apperror.RecordNotFound(errorMessage)
}

return &collections, total, err
},
)
Expand Down
28 changes: 22 additions & 6 deletions internal/usecase/district_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,34 @@ func (uc *District) GetById(ctx context.Context, request model.GetDistrictByIDRe
return appusecase.Wrapper[entity.District](
appusecase.NewContext(ctx, uc.Log, uc.DB, request),
func(ctx *appusecase.Context[model.GetDistrictByIDRequest[[]int]]) (*[]entity.District, int64, error) {
id := ctx.Request.ID
idCity := ctx.Request.IDCity
idProvince := ctx.Request.IDProvince

where := map[string]interface{}{}
if ctx.Request.ID != nil {
where["id"] = ctx.Request.ID
if id != nil {
where["id"] = id
}
if ctx.Request.IDCity != nil {
where["id_city"] = ctx.Request.IDCity
if idCity != nil {
where["id_city"] = idCity
}
if ctx.Request.IDProvince != nil {
where["id_province"] = ctx.Request.IDProvince
if idProvince != nil {
where["id_province"] = idProvince
}

collections, total, err := uc.Repository.FindAndCountBy(ctx.DB, where)

if len(collections) == 0 {
errorMessage := fmt.Sprintf("failed to get cities data with ID: %d", id)
if idCity != nil {
errorMessage += fmt.Sprintf(" and ID City: %d", idCity)
}
if idProvince != nil {
errorMessage += fmt.Sprintf(" and ID Province: %d", idProvince)
}
return nil, 0, apperror.RecordNotFound(errorMessage)
}

return &collections, total, err
},
)
Expand Down
35 changes: 29 additions & 6 deletions internal/usecase/village_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,41 @@ func (uc *Village) GetById(ctx context.Context, request model.GetVillageByIDRequ
return appusecase.Wrapper[entity.Village](
appusecase.NewContext(ctx, uc.Log, uc.DB, request),
func(ctx *appusecase.Context[model.GetVillageByIDRequest[[]int]]) (*[]entity.Village, int64, error) {
id := ctx.Request.ID
idDistrict := ctx.Request.IDDistrict
idCity := ctx.Request.IDCity
idProvince := ctx.Request.IDProvince

where := map[string]interface{}{}
if ctx.Request.ID != nil {
where["id"] = ctx.Request.ID
if id != nil {
where["id"] = id
}
if ctx.Request.IDCity != nil {
where["id_city"] = ctx.Request.IDCity
if idDistrict != nil {
where["id_district"] = idDistrict
}
if ctx.Request.IDProvince != nil {
where["id_province"] = ctx.Request.IDProvince
if idCity != nil {
where["id_city"] = idCity
}
if idProvince != nil {
where["id_province"] = idProvince
}

collections, total, err := uc.Repository.FindAndCountBy(ctx.DB, where)

if len(collections) == 0 {
errorMessage := fmt.Sprintf("failed to get cities data with ID: %d", id)
if idDistrict != nil {
errorMessage += fmt.Sprintf(" and ID District: %d", idDistrict)
}
if idCity != nil {
errorMessage += fmt.Sprintf(" and ID City: %d", idCity)
}
if idProvince != nil {
errorMessage += fmt.Sprintf(" and ID Province: %d", idProvince)
}
return nil, 0, apperror.RecordNotFound(errorMessage)
}

return &collections, total, err
},
)
Expand Down
40 changes: 22 additions & 18 deletions pkg/delivery/http/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,18 @@ func Wrapper[TRequest any, TEntity any, TModel any](ctx *Context[TRequest, TEnti
func buildResponse[TRequest any, TEntity any, TModel any](ctx *Context[TRequest, TEntity, TModel]) error {
data := ctx.Data

// Handle case where collection is a single TEntity
if item, ok := data.collection.(*TEntity); ok {
return ctx.FiberCtx.JSON(
model.WebResponse[TModel]{
Data: *ctx.Mapper.ModelToResponse(item),
},
)
}

// Handle case where collection is a slice of TEntity
if collection, ok := data.collection.([]TEntity); ok {
collectionValue := reflect.ValueOf(data.collection).Elem()
if collection, ok := collectionValue.Interface().([]TEntity); ok {
responses := make([]TModel, len(collection))
for i, item := range collection {
responses[i] = *ctx.Mapper.ModelToResponse(&item)
Expand All @@ -73,15 +83,6 @@ func buildResponse[TRequest any, TEntity any, TModel any](ctx *Context[TRequest,
)
}

// Handle case where collection is a single TEntity
if item, ok := data.collection.(*TEntity); ok {
return ctx.FiberCtx.JSON(
model.WebResponse[TModel]{
Data: *ctx.Mapper.ModelToResponse(item),
},
)
}

return fiber.ErrInternalServerError
}

Expand Down Expand Up @@ -109,14 +110,17 @@ func parseRequest(ctx *fiber.Ctx, request any) error {
func generatePageMeta(request any, total int64) *model.PageMetadata {
r := reflect.ValueOf(request)
for i := 0; i < r.NumField(); i++ {
if pagination, ok := r.Field(i).Interface().(model.PageRequest); ok {
if pagination.Page > 0 && pagination.Size > 0 {
return &model.PageMetadata{
Page: pagination.Page,
Size: pagination.Size,
TotalItem: total,
TotalPage: int64(math.Ceil(float64(total) / float64(pagination.Size))),
}
pagination, ok := r.Field(i).Interface().(model.PageRequest)
if !ok {
return nil
}

if pagination.Page > 0 && pagination.Size > 0 {
return &model.PageMetadata{
Page: pagination.Page,
Size: pagination.Size,
TotalItem: total,
TotalPage: int64(math.Ceil(float64(total) / float64(pagination.Size))),
}
}
}
Expand Down
73 changes: 47 additions & 26 deletions pkg/usecase/usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,16 @@ func NewContext[T any](ctx context.Context, log *zap.Logger, db *gorm.DB, reques
type Callback[TEntity any, TRequest any, TResult any] func(ctx *Context[TRequest]) (*TResult, int64, error)

func Wrapper[TEntity any, TRequest any, TResult any](ctx *Context[TRequest], callback Callback[TEntity, TRequest, TResult]) (*TResult, int64, error) {
var err error
ctx.DB, err = addRelations(ctx.DB, generateRelations[TEntity](ctx.DB), ctx.Request)
if err != nil {
return nil, 0, err
var apperr *apperror.CustomErrorResponse
ctx.DB, apperr = addRelations(ctx.DB, generateRelations[TEntity](ctx.DB), ctx.Request)
if apperr != nil {
if apperr.HTTPCode != 500 {
return nil, 0, apperr
}

errorMessage := "failed to process its relation"
applog.Write(ctx.Log, ctx.Ctx, fmt.Sprintf("%v: ", errorMessage), apperr)
return nil, 0, apperror.InternalServerError(errorMessage)
}
ctx.DB = addPagination(ctx.DB, ctx.Request)

Expand Down Expand Up @@ -102,37 +108,52 @@ func collectRelations(db *gorm.DB, collection any) *relations {
return &relations{pascal: relations_pascal, snake: relations_snake}
}

func addRelations(db *gorm.DB, relations *relations, request any) (*gorm.DB, error) {
func addRelations(db *gorm.DB, relations *relations, request any) (*gorm.DB, *apperror.CustomErrorResponse) {
r := reflect.ValueOf(request)
if r.FieldByName("Include").IsValid() {
if include, ok := r.FieldByName("Include").Interface().([]string); ok {
for _, relation := range include {
if strings.Contains(relation, ".") {
// TODO: Check if the relation is valid
str := stringy.New(relation)
db = db.Preload(str.PascalCase().Delimited(".").Get())
} else {
idx := slice.ArrayIndexOf(relations.snake, relation)
if idx == -1 {
return nil, apperror.BadRequest(fmt.Sprintf("Invalid relation: %v provided. Available relations are [%v].", relation, strings.Join(relations.snake, ", ")))
}
db = db.Preload(relations.pascal[idx])
}

if !r.FieldByName("Include").IsValid() {
return db, nil
}

include, ok := r.FieldByName("Include").Interface().([]string)
if !ok {
return nil, apperror.InternalServerError("Invalid type for 'Include' field. Expected []string.")
}

for _, relation := range include {
if strings.Contains(relation, ".") {
// TODO: Implement relation validation logic, Validate if relPath is a valid relation
str := stringy.New(relation)
db = db.Preload(str.PascalCase().Delimited(".").Get())
} else {
idx := slice.ArrayIndexOf(relations.snake, relation)
if idx == -1 {
return nil, apperror.BadRequest(fmt.Sprintf("Invalid relation: %v provided. Available relations are [%v].", relation, strings.Join(relations.snake, ", ")))
}
db = db.Preload(relations.pascal[idx])
}
}

return db, nil
}

func addPagination(db *gorm.DB, request any) *gorm.DB {
r := reflect.ValueOf(request)
for i := 0; i < r.NumField(); i++ {
if pagination, ok := r.Field(i).Interface().(model.PageRequest); ok {
if pagination.Page > 0 && pagination.Size > 0 {
offset := (pagination.Page - 1) * pagination.Size
return db.Offset(offset).Limit(pagination.Size)
}
}

pageRequestField := r.FieldByName("PageRequest")
if !pageRequestField.IsValid() || pageRequestField.Kind() != reflect.Struct {
return db
}

pagination, ok := pageRequestField.Interface().(model.PageRequest)
if !ok {
return db
}

if pagination.Page > 0 && pagination.Size > 0 {
offset := (pagination.Page - 1) * pagination.Size
return db.Offset(offset).Limit(pagination.Size)
}

return db
}
Loading

0 comments on commit 4b66723

Please sign in to comment.