Compare commits
	
		
			7 Commits
		
	
	
		
			be7f57d5a1
			...
			addddb152a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| addddb152a | |||
| 3e09afd4b0 | |||
| c312b4e2c8 | |||
| eee5084821 | |||
| ce3076047a | |||
| 9b6282a101 | |||
| a3c2ade9fb | 
							
								
								
									
										1
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Makefile
									
									
									
									
									
								
							| @ -37,6 +37,7 @@ build: tidy sqlc # build. | ||||
| 	@go build -v -ldflags "$(GO_LDFLAGS)" -o $(OUTPUT_DIR)/howmuch $(ROOT_DIR)/cmd/howmuch/main.go 2>/dev/null | ||||
|  | ||||
| .PHONY: sqlc | ||||
| sqlc: | ||||
| 	@sqlc generate | ||||
|  | ||||
| .PHONY: format | ||||
|  | ||||
							
								
								
									
										62
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										62
									
								
								README.md
									
									
									
									
									
								
							| @ -54,11 +54,11 @@ Next I need to design the API. | ||||
|   - add other users to that event | ||||
|   - A user can only view their own events, but not the events of other users' | ||||
|   - A user can add an expense to the event (reason, date, who payed how much, | ||||
|   who benefited how much) | ||||
|     who benefited how much) | ||||
|   - Users in the event can edit or delete one entry | ||||
|   - changes are sent to friends in the event | ||||
|   - User can get the money they spent themselves and the money they must pay | ||||
|   to each other | ||||
|     to each other | ||||
|   - User can also get the total amount or the histories. | ||||
|  | ||||
| That is what I thought of for now. | ||||
| @ -102,10 +102,10 @@ The execution of the program is then just a command like `howmuch run`. | ||||
| Moreover, in a distributed system, configs can be stored on `etcd`. | ||||
|  | ||||
| > [Kubernetes stores configuration data into etcd for service discovery and | ||||
| cluster management; etcd’s consistency is crucial for correctly scheduling | ||||
| and operating services. The Kubernetes API server persists cluster state | ||||
| into etcd. It uses etcd’s watch API to monitor the cluster and roll out | ||||
| critical configuration changes.](https://etcd.io/docs/v3.5/learning/why/) | ||||
| > cluster management; etcd’s consistency is crucial for correctly scheduling | ||||
| > and operating services. The Kubernetes API server persists cluster state | ||||
| > into etcd. It uses etcd’s watch API to monitor the cluster and roll out | ||||
| > critical configuration changes.](https://etcd.io/docs/v3.5/learning/why/) | ||||
|  | ||||
| #### Business logic | ||||
|  | ||||
| @ -113,8 +113,8 @@ critical configuration changes.](https://etcd.io/docs/v3.5/learning/why/) | ||||
| - init DBs (Redis, SQL, Kafka, etc.) | ||||
| - init web service (http, https, gRPC, etc.) | ||||
| - start async tasks like `watch kube-apiserver`; pull data from third-party | ||||
| services; store, register `/metrics` and listen on some port; start kafka | ||||
| consumer queue, etc. | ||||
|   services; store, register `/metrics` and listen on some port; start kafka | ||||
|   consumer queue, etc. | ||||
| - Run specific business logic | ||||
| - Stop the program | ||||
| - others... | ||||
| @ -166,26 +166,26 @@ that has several layers: | ||||
| - Entities: the models of the product | ||||
| - Use cases: the core business rule | ||||
| - Interface Adapters: convert data-in to entities and convert data-out to | ||||
| output ports. | ||||
|   output ports. | ||||
| - Frameworks and drivers: Web server, DB. | ||||
|  | ||||
| Based on this logic, we create the following directories: | ||||
|  | ||||
| - `model`: entities | ||||
| - `infra`: Provides the necessary functions to setup the infrastructure, | ||||
| especially the DB (output-port), but also the router (input-port). Once | ||||
| setup, we don't touch them anymore. | ||||
|   especially the DB (output-port), but also the router (input-port). Once | ||||
|   setup, we don't touch them anymore. | ||||
| - `registry`: Provides a register function for the main to register a service. | ||||
| It takes the pass to the output-port (ex.DBs) and gives back a pass | ||||
| (controller) to the input-port | ||||
|   It takes the pass to the output-port (ex.DBs) and gives back a pass | ||||
|   (controller) to the input-port | ||||
| - `adapter`: Controllers are one of the adapters, when they are called, | ||||
| they parse the user input and parse them into models and run the usecase | ||||
| rules. Then they send back the response(input-port). For the output-port | ||||
| part, the `repo` is the implementation of interfaces defined in `usecase/repo`. | ||||
|   they parse the user input and parse them into models and run the usecase | ||||
|   rules. Then they send back the response(input-port). For the output-port | ||||
|   part, the `repo` is the implementation of interfaces defined in `usecase/repo`. | ||||
| - `usecase`: with the input of adapter, do what have to be done, and answer | ||||
| with the result. In the meantime, we may have to store things into DBs. | ||||
| Here we use the Repository model to decouple the implementation of the repo | ||||
| with the interface. Thus in `usecase/repo` we only define interfaces. | ||||
|   with the result. In the meantime, we may have to store things into DBs. | ||||
|   Here we use the Repository model to decouple the implementation of the repo | ||||
|   with the interface. Thus in `usecase/repo` we only define interfaces. | ||||
|  | ||||
| Then it comes the real design for the app. | ||||
|  | ||||
| @ -219,17 +219,17 @@ For the test-driven part, | ||||
| - infra: routes and db connections, it works when it works. Nothing to test. | ||||
| - registry: Just return some structs, no logic. **Not worth testing** | ||||
| - adapter: | ||||
|     - input-port (controller) test: it is about testing parsing the input | ||||
|   - input-port (controller) test: it is about testing parsing the input | ||||
|     value, and the output results writing. The unit test of controller is to | ||||
|     **make sure that they behave as defined in the API documentation**. To | ||||
|     test, we have to mock the **business service**. | ||||
|     - output-port (repo) test: it is about testing converting business model | ||||
|   - output-port (repo) test: it is about testing converting business model | ||||
|     to database model and the interaction with the database. If we are going | ||||
|     to test them, it's about simulating different type of database behaviour | ||||
|     (success, timeout, etc.). To test, we have to mock the | ||||
|     **database connection**. | ||||
| - usecase: This is the core part to test, it's about the core business. | ||||
| We provide the data input and we check the data output in a fake repository. | ||||
|   We provide the data input and we check the data output in a fake repository. | ||||
|  | ||||
| With this design, although it may seem overkill for this little project, fits | ||||
| perfectly well with the TDD method. | ||||
| @ -253,7 +253,7 @@ integration test part (the 2nd point). | ||||
|  | ||||
| I rethought about the whole API design (even though I have only one yet). I | ||||
| have created `/signup` and `/login` without thinking too much, but in fact | ||||
| it is not quite *RESTful*. | ||||
| it is not quite _RESTful_. | ||||
|  | ||||
| **REST** is all about resources. While `/signup` and `/login` is quite | ||||
| comprehensible, thus service-oriented, they don't follow the REST philosophy, | ||||
| @ -292,8 +292,8 @@ choose server-side-rendering with `templ + htmx`, or even `template+vanilla | ||||
| javascript`. | ||||
|  | ||||
| I can still write a rather static Go-frontend-server to serve HTMLs and call | ||||
| my Go backend. *And it might be a good idea if they communicate on Go native | ||||
| rpc.* It worth a try. | ||||
| my Go backend. _And it might be a good idea if they communicate on Go native | ||||
| rpc._ It worth a try. | ||||
|  | ||||
| And I have moved on to `Svelte` which seems very simple by design and the | ||||
| whole compile thing makes it really charm. But this is mainly a Go project, | ||||
| @ -330,3 +330,15 @@ the database model design. | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 2024/10/11 | ||||
|  | ||||
| I spent 2 days learning some basic of Vue. Learning Vue takes time. There | ||||
| are a lot of concepts and it needs a lot of practice. Even though I may not | ||||
| need a professional level web page, I don't want to copy one module from this | ||||
| blog and another one from another tutorial. I might just put aside the | ||||
| front-end for now and concentrate on my backend Go app. | ||||
|  | ||||
| For now, I will just test my backend with `curl`. | ||||
|  | ||||
| And today's job is to get the login part done! | ||||
|  | ||||
| @ -77,6 +77,7 @@ paths: | ||||
|     post: | ||||
|       tags: | ||||
|         - user | ||||
|         - session | ||||
|       description: Create a new session for a user | ||||
|       requestBody: | ||||
|         description: Create session | ||||
|  | ||||
| @ -28,4 +28,6 @@ type AppController struct { | ||||
| 	User interface{ User } | ||||
|  | ||||
| 	Admin interface{ Admin } | ||||
|  | ||||
| 	Session interface{ Session } | ||||
| } | ||||
|  | ||||
							
								
								
									
										20
									
								
								internal/howmuch/adapter/controller/session.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								internal/howmuch/adapter/controller/session.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| package controller | ||||
|  | ||||
| import ( | ||||
| 	"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| type Session interface { | ||||
| 	Create(*gin.Context) | ||||
| } | ||||
|  | ||||
| type SessionController struct{} | ||||
|  | ||||
| func NewSessionController() Session { | ||||
| 	return &SessionController{} | ||||
| } | ||||
|  | ||||
| func (sc *SessionController) Create(ctx *gin.Context) { | ||||
| 	log.CtxLog(ctx).DebugLog("session create") | ||||
| } | ||||
| @ -25,3 +25,8 @@ INSERT INTO "user" ( | ||||
|     email, first_name, last_name, password, created_at, updated_at | ||||
| ) VALUES ( $1, $2, $3, $4, $5, $6 ) | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: GetUserByEmail :one | ||||
| SELECT id, email, first_name, last_name, password, created_at, updated_at | ||||
|     FROM "user" | ||||
|     WHERE email = $1; | ||||
|  | ||||
| @ -11,6 +11,27 @@ import ( | ||||
| 	"github.com/jackc/pgx/v5/pgtype" | ||||
| ) | ||||
|  | ||||
| const getUserByEmail = `-- name: GetUserByEmail :one | ||||
| SELECT id, email, first_name, last_name, password, created_at, updated_at | ||||
|     FROM "user" | ||||
|     WHERE email = $1 | ||||
| ` | ||||
|  | ||||
| func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { | ||||
| 	row := q.db.QueryRow(ctx, getUserByEmail, email) | ||||
| 	var i User | ||||
| 	err := row.Scan( | ||||
| 		&i.ID, | ||||
| 		&i.Email, | ||||
| 		&i.FirstName, | ||||
| 		&i.LastName, | ||||
| 		&i.Password, | ||||
| 		&i.CreatedAt, | ||||
| 		&i.UpdatedAt, | ||||
| 	) | ||||
| 	return i, err | ||||
| } | ||||
|  | ||||
| const insertUser = `-- name: InsertUser :one | ||||
|  | ||||
| INSERT INTO "user" ( | ||||
|  | ||||
| @ -86,3 +86,25 @@ func (ur *userRepository) Create( | ||||
| 		UpdatedAt: userDB.CreatedAt.Time, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // GetByEmail if not found, return nil for user but not error. | ||||
| func (ur *userRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) { | ||||
| 	queries := sqlc.New(ur.db) | ||||
| 	userDB, err := queries.GetUserByEmail(ctx, email) | ||||
| 	if errors.Is(err, pgx.ErrNoRows) { | ||||
| 		// No query error, but user not found | ||||
| 		return nil, nil | ||||
| 	} else if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &model.User{ | ||||
| 		ID:        int(userDB.ID), | ||||
| 		Email:     userDB.Email, | ||||
| 		FirstName: userDB.FirstName, | ||||
| 		LastName:  userDB.LastName, | ||||
| 		Password:  userDB.Password, | ||||
| 		CreatedAt: userDB.CreatedAt.Time, | ||||
| 		UpdatedAt: userDB.CreatedAt.Time, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| @ -54,6 +54,8 @@ func Routes(engine *gin.Engine, c controller.AppController) *gin.Engine { | ||||
| 		{ | ||||
| 			userV1.POST("/create", func(ctx *gin.Context) { c.User.Create(ctx) }) | ||||
| 		} | ||||
|  | ||||
| 		v1.POST("/session/create", func(ctx *gin.Context) { c.Session.Create(ctx) }) | ||||
| 	} | ||||
|  | ||||
| 	return engine | ||||
|  | ||||
| @ -51,7 +51,8 @@ func NewRegistry(db *pgx.Conn) Registry { | ||||
| // each domain. | ||||
| func (r *registry) NewAppController() controller.AppController { | ||||
| 	return controller.AppController{ | ||||
| 		User:  r.NewUserController(), | ||||
| 		Admin: r.NewAdminController(), | ||||
| 		User:    r.NewUserController(), | ||||
| 		Admin:   r.NewAdminController(), | ||||
| 		Session: r.NewSessionController(), | ||||
| 	} | ||||
| } | ||||
|  | ||||
							
								
								
									
										9
									
								
								internal/howmuch/registry/session.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								internal/howmuch/registry/session.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| package registry | ||||
|  | ||||
| import "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/controller" | ||||
|  | ||||
| // NewSessionController returns a session controller's implementation | ||||
| func (r *registry) NewSessionController() controller.Session { | ||||
| 	// u := usecase.NewSessionUsecase(repo.NewSessionRepository(r.db), repo.NewDBRepository(r.db)) | ||||
| 	return controller.NewSessionController() | ||||
| } | ||||
| @ -30,4 +30,5 @@ import ( | ||||
|  | ||||
| type UserRepository interface { | ||||
| 	Create(ctx context.Context, transaction interface{}, u *model.User) (*model.User, error) | ||||
| 	GetByEmail(ctx context.Context, email string) (*model.User, error) | ||||
| } | ||||
|  | ||||
| @ -20,7 +20,7 @@ | ||||
| // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| // SOFTWARE. | ||||
| 
 | ||||
| package repo | ||||
| package usecase | ||||
| 
 | ||||
| import "context" | ||||
| 
 | ||||
| @ -20,15 +20,18 @@ | ||||
| // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| // SOFTWARE. | ||||
| 
 | ||||
| package repo | ||||
| package usecase | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
| 
 | ||||
| var UserTestDummyErr = errors.New("dummy error") | ||||
| 
 | ||||
| type TestUserRepository struct{} | ||||
| 
 | ||||
| func (tur *TestUserRepository) Create( | ||||
| @ -46,3 +49,21 @@ func (tur *TestUserRepository) Create( | ||||
| 
 | ||||
| 	return &user, nil | ||||
| } | ||||
| 
 | ||||
| func (ur *TestUserRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) { | ||||
| 	hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12) | ||||
| 	switch email { | ||||
| 	case "a@b.c": | ||||
| 		return &model.User{ | ||||
| 			ID:       123, | ||||
| 			Email:    "a@b.c", | ||||
| 			Password: string(hashedPwd), | ||||
| 		}, nil | ||||
| 	case "query@error.com": | ||||
| 		return nil, UserTestDummyErr | ||||
| 	case "inexist@error.com": | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 
 | ||||
| 	return nil, UserTestDummyErr | ||||
| } | ||||
| @ -24,6 +24,7 @@ package usecase | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"regexp" | ||||
| @ -35,11 +36,23 @@ import ( | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
|  | ||||
| var UserExisted = &errno.Errno{ | ||||
| 	HTTP:    http.StatusBadRequest, | ||||
| 	Code:    errno.ErrorCode(errno.FailedOperationCode, "UserExisted"), | ||||
| 	Message: "email already existed.", | ||||
| } | ||||
| var ( | ||||
| 	UserExisted = &errno.Errno{ | ||||
| 		HTTP:    http.StatusBadRequest, | ||||
| 		Code:    errno.ErrorCode(errno.FailedOperationCode, "UserExisted"), | ||||
| 		Message: "email already existed.", | ||||
| 	} | ||||
| 	UserNotExist = &errno.Errno{ | ||||
| 		HTTP:    http.StatusBadRequest, | ||||
| 		Code:    errno.ErrorCode(errno.ResourceNotFoundCode, "UserNotExist"), | ||||
| 		Message: "user does not exists.", | ||||
| 	} | ||||
| 	UserWrongPassword = &errno.Errno{ | ||||
| 		HTTP:    http.StatusBadRequest, | ||||
| 		Code:    errno.ErrorCode(errno.AuthFailureCode, "UserWrongPassword"), | ||||
| 		Message: "wrong password.", | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| type userUsecase struct { | ||||
| 	userRepo repo.UserRepository | ||||
| @ -48,6 +61,7 @@ type userUsecase struct { | ||||
|  | ||||
| type User interface { | ||||
| 	Create(ctx context.Context, u *model.User) (*model.User, error) | ||||
| 	Exist(ctx context.Context, u *model.User) (bool, error) | ||||
| } | ||||
|  | ||||
| func NewUserUsecase(r repo.UserRepository, d repo.DBRepository) User { | ||||
| @ -98,3 +112,24 @@ func (uuc *userUsecase) Create(ctx context.Context, u *model.User) (*model.User, | ||||
|  | ||||
| 	return user, nil | ||||
| } | ||||
|  | ||||
| func (uuc *userUsecase) Exist(ctx context.Context, u *model.User) (bool, error) { | ||||
| 	got, err := uuc.userRepo.GetByEmail(ctx, u.Email) | ||||
| 	// Any query error? | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	// User exists? | ||||
| 	if got == nil { | ||||
| 		return false, UserNotExist | ||||
| 	} | ||||
|  | ||||
| 	// Password correct? | ||||
| 	err = bcrypt.CompareHashAndPassword([]byte(got.Password), []byte(u.Password)) | ||||
| 	if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { | ||||
| 		return false, UserWrongPassword | ||||
| 	} | ||||
|  | ||||
| 	return true, nil | ||||
| } | ||||
|  | ||||
| @ -27,14 +27,13 @@ import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model" | ||||
| 	"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestCreateUser(t *testing.T) { | ||||
| 	t.Run("normal create", func(t *testing.T) { | ||||
| 		ctx := context.Background() | ||||
| 		userUsecase := NewUserUsecase(&repo.TestUserRepository{}, &repo.TestDBRepository{}) | ||||
| 		userUsecase := NewUserUsecase(&TestUserRepository{}, &TestDBRepository{}) | ||||
| 		input := &model.User{ | ||||
| 			Email:     "a@b.c", | ||||
| 			FirstName: "James", | ||||
| @ -51,7 +50,7 @@ func TestCreateUser(t *testing.T) { | ||||
|  | ||||
| 	t.Run("duplicate create", func(t *testing.T) { | ||||
| 		ctx := context.Background() | ||||
| 		userUsecase := NewUserUsecase(&repo.TestUserRepository{}, &repo.TestDBRepository{}) | ||||
| 		userUsecase := NewUserUsecase(&TestUserRepository{}, &TestDBRepository{}) | ||||
| 		input := &model.User{ | ||||
| 			Email:     "duplicate@error.com", | ||||
| 			FirstName: "James", | ||||
| @ -63,3 +62,38 @@ func TestCreateUser(t *testing.T) { | ||||
| 		assert.EqualError(t, err, UserExisted.Error()) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestUserExist(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		Name   string | ||||
| 		User   *model.User | ||||
| 		ExpErr error | ||||
| 		ExpRes bool | ||||
| 	}{ | ||||
| 		{"user exists", &model.User{ | ||||
| 			Email:    "a@b.c", | ||||
| 			Password: "strongHashed", | ||||
| 		}, nil, true}, | ||||
| 		{"query error", &model.User{ | ||||
| 			Email:    "query@error.com", | ||||
| 			Password: "strongHashed", | ||||
| 		}, UserTestDummyErr, false}, | ||||
| 		{"user doesn not exist", &model.User{ | ||||
| 			Email:    "inexist@error.com", | ||||
| 			Password: "strongHashed", | ||||
| 		}, UserNotExist, false}, | ||||
| 		{"wrong password", &model.User{ | ||||
| 			Email:    "a@b.c", | ||||
| 			Password: "wrongHashed", | ||||
| 		}, UserWrongPassword, false}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tst := range testCases { | ||||
| 		ctx := context.Background() | ||||
| 		userUsecase := NewUserUsecase(&TestUserRepository{}, &TestDBRepository{}) | ||||
|  | ||||
| 		got, err := userUsecase.Exist(ctx, tst.User) | ||||
| 		assert.ErrorIs(t, err, tst.ExpErr) | ||||
| 		assert.Equal(t, tst.ExpRes, got) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -33,6 +33,7 @@ type Context interface { | ||||
|  | ||||
| 	// Request | ||||
| 	Bind(obj any) error | ||||
| 	GetHeader(key string) string | ||||
|  | ||||
| 	// Response | ||||
| 	JSON(code int, obj any) | ||||
|  | ||||
| @ -26,6 +26,8 @@ import ( | ||||
| 	"os" | ||||
| 	"sync" | ||||
|  | ||||
| 	"git.vinchent.xyz/vinchent/howmuch/internal/pkg/core" | ||||
| 	"git.vinchent.xyz/vinchent/howmuch/internal/pkg/middleware" | ||||
| 	"go.uber.org/zap" | ||||
| 	"go.uber.org/zap/zapcore" | ||||
| ) | ||||
| @ -100,6 +102,26 @@ func NewLogger(opts *Options) *zapLogger { | ||||
| 	return &zapLogger{z: z} | ||||
| } | ||||
|  | ||||
| // CtxLog writes context's information into the log | ||||
| func CtxLog(ctx core.Context) *zapLogger { | ||||
| 	return std.CtxLog(ctx) | ||||
| } | ||||
|  | ||||
| func (z *zapLogger) CtxLog(ctx core.Context) *zapLogger { | ||||
| 	zz := z.clone() | ||||
|  | ||||
| 	if rid := ctx.GetHeader(middleware.XRequestID); rid != "" { | ||||
| 		zz.z = zz.z.With(zap.Any(middleware.XRequestID, rid)) | ||||
| 	} | ||||
|  | ||||
| 	return zz | ||||
| } | ||||
|  | ||||
| func (z *zapLogger) clone() *zapLogger { | ||||
| 	zz := *z | ||||
| 	return &zz | ||||
| } | ||||
|  | ||||
| func (z *zapLogger) FatalLog(msg string, keyValues ...interface{}) { | ||||
| 	z.z.Sugar().Fatalw(msg, keyValues...) | ||||
| } | ||||
|  | ||||
| @ -27,20 +27,20 @@ import ( | ||||
| 	"github.com/google/uuid" | ||||
| ) | ||||
|  | ||||
| const requestID = "X-Request-Id" | ||||
| const XRequestID = "X-Request-Id" | ||||
|  | ||||
| func RequestID() gin.HandlerFunc { | ||||
| 	return func(ctx *gin.Context) { | ||||
| 		var rid string | ||||
|  | ||||
| 		if rid = ctx.GetHeader(requestID); rid != "" { | ||||
| 			ctx.Request.Header.Add(requestID, rid) | ||||
| 		if rid = ctx.GetHeader(XRequestID); rid != "" { | ||||
| 			ctx.Request.Header.Add(XRequestID, rid) | ||||
| 			ctx.Next() | ||||
| 		} | ||||
|  | ||||
| 		rid = uuid.NewString() | ||||
| 		ctx.Request.Header.Add(requestID, rid) | ||||
| 		ctx.Header(requestID, rid) | ||||
| 		ctx.Request.Header.Add(XRequestID, rid) | ||||
| 		ctx.Header(XRequestID, rid) | ||||
| 		ctx.Next() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -24,33 +24,14 @@ package middleware | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
|  | ||||
| 	"git.vinchent.xyz/vinchent/howmuch/internal/pkg/test" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/google/uuid" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| type header struct { | ||||
| 	Key   string | ||||
| 	Value string | ||||
| } | ||||
|  | ||||
| func performRequest( | ||||
| 	r http.Handler, | ||||
| 	method, path string, | ||||
| 	headers ...header, | ||||
| ) *httptest.ResponseRecorder { | ||||
| 	req := httptest.NewRequest(method, path, nil) | ||||
| 	for _, h := range headers { | ||||
| 		req.Header.Add(h.Key, h.Value) | ||||
| 	} | ||||
| 	w := httptest.NewRecorder() | ||||
| 	r.ServeHTTP(w, req) | ||||
| 	return w | ||||
| } | ||||
|  | ||||
| func TestRequestID(t *testing.T) { | ||||
| 	r := gin.New() | ||||
| 	r.Use(RequestID()) | ||||
| @ -58,21 +39,28 @@ func TestRequestID(t *testing.T) { | ||||
| 	wanted := "123" | ||||
|  | ||||
| 	r.GET("/example", func(c *gin.Context) { | ||||
| 		got = c.GetHeader(requestID) | ||||
| 		got = c.GetHeader(XRequestID) | ||||
| 		c.Status(http.StatusOK) | ||||
| 	}) | ||||
|  | ||||
| 	r.POST("/example", func(c *gin.Context) { | ||||
| 		got = c.GetHeader(requestID) | ||||
| 		got = c.GetHeader(XRequestID) | ||||
| 		c.String(http.StatusAccepted, "ok") | ||||
| 	}) | ||||
|  | ||||
| 	// Test with Request ID | ||||
| 	_ = performRequest(r, "GET", "/example?a=100", header{requestID, wanted}) | ||||
| 	_ = test.PerformRequest( | ||||
| 		t, | ||||
| 		r, | ||||
| 		"GET", | ||||
| 		"/example?a=100", | ||||
| 		nil, | ||||
| 		test.Header{Key: XRequestID, Value: wanted}, | ||||
| 	) | ||||
| 	assert.Equal(t, "123", got) | ||||
|  | ||||
| 	res := performRequest(r, "GET", "/example?a=100") | ||||
| 	res := test.PerformRequest(t, r, "GET", "/example?a=100", nil) | ||||
| 	assert.NotEqual(t, "", got) | ||||
| 	assert.NoError(t, uuid.Validate(got)) | ||||
| 	assert.Equal(t, res.Header()[requestID][0], got) | ||||
| 	assert.Equal(t, res.Header()[XRequestID][0], got) | ||||
| } | ||||
|  | ||||
							
								
								
									
										30
									
								
								internal/pkg/test/request.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								internal/pkg/test/request.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| package test | ||||
|  | ||||
| import ( | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| type Header struct { | ||||
| 	Key   string | ||||
| 	Value string | ||||
| } | ||||
|  | ||||
| func PerformRequest( | ||||
| 	t testing.TB, | ||||
| 	r http.Handler, | ||||
| 	method, path string, | ||||
| 	body io.Reader, | ||||
| 	headers ...Header, | ||||
| ) *httptest.ResponseRecorder { | ||||
| 	t.Helper() | ||||
| 	req := httptest.NewRequest(method, path, body) | ||||
| 	for _, h := range headers { | ||||
| 		req.Header.Add(h.Key, h.Value) | ||||
| 	} | ||||
| 	w := httptest.NewRecorder() | ||||
| 	r.ServeHTTP(w, req) | ||||
| 	return w | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	