From 0b3593fc4a6d4768843afda06d3ddec8019f8569 Mon Sep 17 00:00:00 2001 From: Frankie Jun Date: Sat, 30 Sep 2023 22:31:47 +0800 Subject: [PATCH] =?UTF-8?q?Issues:=E7=94=A8=E6=88=B7=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=20#22=20(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * email service 简单的 failvover 策略 * e2e * test * 增加zap日志 * 增加zap日志 * 增加zap日志 * 实现个人信息编辑接口 * 实现个人信息编辑接口 * 实现个人信息编辑接口 * 实现个人信息编辑接口-补测试 --------- Co-authored-by: frankiejun --- internal/domain/user.go | 3 + internal/repository/dao/mocks/user.mock.go | 14 +++ internal/repository/dao/user.go | 21 +++- internal/repository/dao/user_test.go | 78 +++++++++++++++ internal/repository/mocks/user.mock.go | 14 +++ internal/repository/user.go | 20 ++++ internal/repository/user_test.go | 54 +++++++++++ internal/service/mocks/user.mock.go | 14 +++ internal/service/user.go | 5 + internal/service/user_test.go | 37 +++++++ internal/web/init_web.go | 1 + internal/web/user.go | 64 +++++++++++- internal/web/user_test.go | 108 +++++++++++++++++++++ 13 files changed, 427 insertions(+), 6 deletions(-) diff --git a/internal/domain/user.go b/internal/domain/user.go index cdb48b00..b4b2bf99 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -21,6 +21,9 @@ type User struct { Email string EmailVerified bool Password string + NickName string + Birthday string + AboutMe string CreateTime time.Time UpdateTime time.Time } diff --git a/internal/repository/dao/mocks/user.mock.go b/internal/repository/dao/mocks/user.mock.go index 22fa9389..61af189f 100644 --- a/internal/repository/dao/mocks/user.mock.go +++ b/internal/repository/dao/mocks/user.mock.go @@ -77,3 +77,17 @@ func (mr *MockUserDAOMockRecorder) UpdateEmailVerified(ctx, email interface{}) * mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEmailVerified", reflect.TypeOf((*MockUserDAO)(nil).UpdateEmailVerified), ctx, email) } + +// UpdateUserProfile mocks base method. +func (m *MockUserDAO) UpdateUserProfile(ctx context.Context, u dao.User) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserProfile", ctx, u) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateUserProfile indicates an expected call of UpdateUserProfile. +func (mr *MockUserDAOMockRecorder) UpdateUserProfile(ctx, u interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserProfile", reflect.TypeOf((*MockUserDAO)(nil).UpdateUserProfile), ctx, u) +} diff --git a/internal/repository/dao/user.go b/internal/repository/dao/user.go index 20ca6d67..a3887629 100644 --- a/internal/repository/dao/user.go +++ b/internal/repository/dao/user.go @@ -2,6 +2,7 @@ package dao import ( "context" + "database/sql" "errors" "time" @@ -16,6 +17,7 @@ var ( type UserDAO interface { Insert(ctx context.Context, u User) error UpdateEmailVerified(ctx context.Context, email string) error + UpdateUserProfile(ctx context.Context, u User) error FindByEmail(ctx context.Context, email string) (User, error) } @@ -24,8 +26,13 @@ type User struct { Email string `gorm:"unique"` EmailVerified bool Password string - CreateTime int64 - UpdateTime int64 + + NickName sql.NullString + Birthday sql.NullString + AboutMe sql.NullString + + CreateTime int64 + UpdateTime int64 } type GormUserDAO struct { @@ -68,3 +75,13 @@ func (dao *GormUserDAO) FindByEmail(ctx context.Context, email string) (User, er err := dao.db.WithContext(ctx).First(&u, "email = ?", email).Error return u, err } + +func (dao *GormUserDAO) UpdateUserProfile(ctx context.Context, u User) error { + now := time.Now().UnixMilli() + return dao.db.WithContext(ctx).Model(&User{}).Where("id=?", u.Id).Updates(map[string]interface{}{ + "nick_name": u.NickName, + "birthday": u.Birthday, + "about_me": u.AboutMe, + "update_time": now}).Error + +} diff --git a/internal/repository/dao/user_test.go b/internal/repository/dao/user_test.go index 87c76791..70f6f5dc 100644 --- a/internal/repository/dao/user_test.go +++ b/internal/repository/dao/user_test.go @@ -197,3 +197,81 @@ func TestGormUserDAO_UpdateEmailVerified(t *testing.T) { }) } } + +func TestGormUserDAO_UpdateUserProfile(t *testing.T) { + testCases := []struct { + name string + ctx context.Context + user User + mock func(t *testing.T) *sql.DB + wantErr error + }{ + { + name: "更新成功", + ctx: context.Background(), + user: User{ + Id: 1, + NickName: sql.NullString{ + String: "frankiejun", + Valid: true, + }, + Birthday: sql.NullString{ + String: "2000-01-01", + Valid: true, + }, + AboutMe: sql.NullString{ + String: "I am a programmer", + Valid: true, + }, + }, + mock: func(t *testing.T) *sql.DB { + mockDB, mock, err := sqlmock.New() + require.NoError(t, err) + mock.ExpectExec("UPDATE `users` .*").WillReturnResult(sqlmock.NewResult(1, 1)) + return mockDB + }, + wantErr: nil, + }, + { + name: "更新失败", + ctx: context.Background(), + user: User{ + Id: 1, + NickName: sql.NullString{ + String: "frankiejun", + Valid: true, + }, + Birthday: sql.NullString{ + String: "2000-01-01", + Valid: true, + }, + AboutMe: sql.NullString{ + String: "I am a programmer", + Valid: true, + }, + }, + mock: func(t *testing.T) *sql.DB { + mockDB, mock, err := sqlmock.New() + require.NoError(t, err) + mock.ExpectExec("UPDATE `users` .*").WillReturnError(errors.New("update failed")) + return mockDB + }, + wantErr: errors.New("update failed"), + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + db, err := gorm.Open(gormMysql.New(gormMysql.Config{ + Conn: tt.mock(t), + SkipInitializeWithVersion: true, + }), &gorm.Config{ + DisableAutomaticPing: true, + SkipDefaultTransaction: true, + }) + assert.NoError(t, err) + dao := NewUserInfoDAO(db) + err = dao.UpdateUserProfile(tt.ctx, tt.user) + assert.Equal(t, tt.wantErr, err) + }) + } +} diff --git a/internal/repository/mocks/user.mock.go b/internal/repository/mocks/user.mock.go index 6d50c38f..fcdd1cee 100644 --- a/internal/repository/mocks/user.mock.go +++ b/internal/repository/mocks/user.mock.go @@ -77,3 +77,17 @@ func (mr *MockUserRepositoryMockRecorder) UpdateEmailVerified(ctx, email interfa mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEmailVerified", reflect.TypeOf((*MockUserRepository)(nil).UpdateEmailVerified), ctx, email) } + +// UpdateUserProfile mocks base method. +func (m *MockUserRepository) UpdateUserProfile(ctx context.Context, u domain.User) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserProfile", ctx, u) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateUserProfile indicates an expected call of UpdateUserProfile. +func (mr *MockUserRepositoryMockRecorder) UpdateUserProfile(ctx, u interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserProfile", reflect.TypeOf((*MockUserRepository)(nil).UpdateUserProfile), ctx, u) +} diff --git a/internal/repository/user.go b/internal/repository/user.go index de1f5e21..ea1948a2 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -2,6 +2,7 @@ package repository import ( "context" + "database/sql" "github.com/ecodeclub/webook/internal/domain" "github.com/ecodeclub/webook/internal/repository/dao" @@ -15,6 +16,7 @@ type UserRepository interface { Create(ctx context.Context, u *domain.User) error UpdateEmailVerified(ctx context.Context, email string) error FindByEmail(ctx context.Context, email string) (domain.User, error) + UpdateUserProfile(ctx context.Context, u domain.User) error } type UserInfoRepository struct { @@ -57,3 +59,21 @@ func (ur *UserInfoRepository) FindByEmail(ctx context.Context, email string) (do } return ur.userToDomain(user), err } + +func (ur *UserInfoRepository) UpdateUserProfile(ctx context.Context, u domain.User) error { + return ur.dao.UpdateUserProfile(ctx, dao.User{ + Id: u.Id, + NickName: sql.NullString{ + String: u.NickName, + Valid: len(u.NickName) > 0, + }, + Birthday: sql.NullString{ + String: u.Birthday, + Valid: len(u.Birthday) > 0, + }, + AboutMe: sql.NullString{ + String: u.AboutMe, + Valid: len(u.AboutMe) > 0, + }, + }) +} diff --git a/internal/repository/user_test.go b/internal/repository/user_test.go index 482d7a50..08d01041 100644 --- a/internal/repository/user_test.go +++ b/internal/repository/user_test.go @@ -2,6 +2,7 @@ package repository import ( "context" + "errors" "testing" "time" @@ -131,3 +132,56 @@ func TestUserInfoRepository_FindByEmail(t *testing.T) { }) } } + +func TestUserInfoRepository_UpdateUserProfile(t *testing.T) { + testCases := []struct { + name string + mock func(*gomock.Controller) dao.UserDAO + ctx context.Context + user domain.User + wantErr error + }{ + { + name: "更新失败", + mock: func(ctrl *gomock.Controller) dao.UserDAO { + d := daomocks.NewMockUserDAO(ctrl) + d.EXPECT().UpdateUserProfile(gomock.Any(), gomock.Any()).Return(errors.New("更新失败")) + return d + }, + ctx: context.Background(), + user: domain.User{ + Id: 1, + NickName: "frankiejun", + AboutMe: "I am a good boy", + Birthday: "1999-01-01", + }, + wantErr: errors.New("更新失败"), + }, + { + name: "更新成功", + mock: func(ctrl *gomock.Controller) dao.UserDAO { + d := daomocks.NewMockUserDAO(ctrl) + d.EXPECT().UpdateUserProfile(gomock.Any(), gomock.Any()).Return(nil) + return d + }, + ctx: context.Background(), + user: domain.User{ + Id: 1, + NickName: "frankiejun", + AboutMe: "I am a good boy", + Birthday: "1999-01-01", + }, + wantErr: nil, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repo := NewUserInfoRepository(tc.mock(ctrl)) + err := repo.UpdateUserProfile(tc.ctx, tc.user) + assert.Equal(t, tc.wantErr, err) + }) + } +} diff --git a/internal/service/mocks/user.mock.go b/internal/service/mocks/user.mock.go index f40d1023..afc287b3 100644 --- a/internal/service/mocks/user.mock.go +++ b/internal/service/mocks/user.mock.go @@ -35,6 +35,20 @@ func (m *MockUserService) EXPECT() *MockUserServiceMockRecorder { return m.recorder } +// EditUserProfile mocks base method. +func (m *MockUserService) EditUserProfile(ctx context.Context, u domain.User) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EditUserProfile", ctx, u) + ret0, _ := ret[0].(error) + return ret0 +} + +// EditUserProfile indicates an expected call of EditUserProfile. +func (mr *MockUserServiceMockRecorder) EditUserProfile(ctx, u interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditUserProfile", reflect.TypeOf((*MockUserService)(nil).EditUserProfile), ctx, u) +} + // SendVerifyEmail mocks base method. func (m *MockUserService) SendVerifyEmail(ctx context.Context, email string) error { m.ctrl.T.Helper() diff --git a/internal/service/user.go b/internal/service/user.go index 3f5d701d..304de05d 100644 --- a/internal/service/user.go +++ b/internal/service/user.go @@ -25,6 +25,7 @@ type UserService interface { Signup(ctx context.Context, u *domain.User) error SendVerifyEmail(ctx context.Context, email string) error VerifyEmail(ctx context.Context, tokenStr string) error + EditUserProfile(ctx context.Context, u domain.User) error } type userService struct { @@ -79,3 +80,7 @@ func (svc *userService) VerifyEmail(ctx context.Context, tokenStr string) error return svc.r.UpdateEmailVerified(ctx, ec.Email) } + +func (svc *userService) EditUserProfile(ctx context.Context, u domain.User) error { + return svc.r.UpdateUserProfile(ctx, u) +} diff --git a/internal/service/user_test.go b/internal/service/user_test.go index d7e9e0ef..99073308 100644 --- a/internal/service/user_test.go +++ b/internal/service/user_test.go @@ -136,3 +136,40 @@ func genToken(emailAddr string, timeout int) string { tokenStr, _ := token.SignedString([]byte(EmailJWTKey)) return tokenStr } + +func Test_userService_EditUserProfile(t *testing.T) { + testCases := []struct { + name string + ctx context.Context + mock func(*gomock.Controller) repository.UserRepository + user domain.User + wantErr error + }{ + { + name: "修改成功", + ctx: context.Background(), + mock: func(ctrl *gomock.Controller) repository.UserRepository { + mock := repomocks.NewMockUserRepository(ctrl) + mock.EXPECT().UpdateUserProfile(gomock.Any(), gomock.Any()).Return(nil) + return mock + }, + user: domain.User{ + Id: 1, + NickName: "frankiejun", + Birthday: "2020-01-01", + AboutMe: "I am a good boy", + }, + wantErr: nil, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + svc := NewUserService(tc.mock(ctrl), nil) + err := svc.EditUserProfile(tc.ctx, tc.user) + assert.Equal(t, tc.wantErr, err) + }) + } +} diff --git a/internal/web/init_web.go b/internal/web/init_web.go index acbb70bd..82480365 100644 --- a/internal/web/init_web.go +++ b/internal/web/init_web.go @@ -6,4 +6,5 @@ func (u *UserHandler) RegisterRoutes(server *gin.Engine) { server.POST("/users/signup", u.SignUp) server.POST("/users/email/verify/:token", u.EmailVerify) server.POST("/users/login", u.Login) + server.POST("/users/edit", u.Edit) } diff --git a/internal/web/user.go b/internal/web/user.go index 9a8c4bfe..010391e5 100644 --- a/internal/web/user.go +++ b/internal/web/user.go @@ -4,6 +4,8 @@ import ( "net/http" "time" + "go.uber.org/zap" + regexp "github.com/dlclark/regexp2" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" @@ -17,11 +19,15 @@ const ( passwordRegexPattern = `^.{6,}$` AccessSecret = "95osj3fUD7fo0mlYdDbncXz4VD2igvf0" RefreshSecret = "95osj3fUD7fo0m123DbncXz4VD2igvf0" + birthdayRegexPatten = `^(19|20)\d\d[-](0[1-9]|1[012])[-](0[1-9]|[12][0-9]|3[01])$` + aboutMeMaxLen = 1024 + nickNameMaxLen = 128 ) type UserHandler struct { - svc service.UserService - passwordRegexExp *regexp.Regexp + svc service.UserService + passwordRegexExp *regexp.Regexp + birthdayRegexPatten *regexp.Regexp } type TokenClaims struct { @@ -34,8 +40,9 @@ type TokenClaims struct { func NewUserHandler(svc service.UserService) *UserHandler { return &UserHandler{ - svc: svc, - passwordRegexExp: regexp.MustCompile(passwordRegexPattern, regexp.None), + svc: svc, + passwordRegexExp: regexp.MustCompile(passwordRegexPattern, regexp.None), + birthdayRegexPatten: regexp.MustCompile(birthdayRegexPatten, regexp.None), } } @@ -157,3 +164,52 @@ func (u *UserHandler) setAccessToken(ctx *gin.Context, fingerprint string, uid i return nil } + +// Edit 用户编译信息 +func (c *UserHandler) Edit(ctx *gin.Context) { + type UserEditReq struct { + Id int64 + NickName string + Birthday string + AboutMe string + } + var req UserEditReq + + if err := ctx.Bind(&req); err != nil { + return + } + + if len(req.NickName) > nickNameMaxLen { + ctx.String(http.StatusOK, "昵称超过长度限制!") + return + } + + //校验生日格式是否合法 + isBirthday, err := c.birthdayRegexPatten.MatchString(req.Birthday) + if err != nil { + ctx.String(http.StatusOK, "系统错误") + return + } + if !isBirthday { + ctx.String(http.StatusOK, + "非法的生日日期,标准样式为:yyyy-mm-dd") + return + } + //校验简介长度 + if len(req.AboutMe) > aboutMeMaxLen { + ctx.String(http.StatusOK, "简介超过长度限制!") + return + } + err = c.svc.EditUserProfile(ctx, domain.User{ + Id: req.Id, + Birthday: req.Birthday, + NickName: req.NickName, + AboutMe: req.AboutMe, + }) + if err != nil { + ctx.String(http.StatusOK, "更新失败!") + zap.L().Error("用户信息更新失败:", zap.Error(err)) + return + } + ctx.String(http.StatusOK, "更新成功") +} diff --git a/internal/web/user_test.go b/internal/web/user_test.go index 1eeb2d7b..3160bf06 100644 --- a/internal/web/user_test.go +++ b/internal/web/user_test.go @@ -357,3 +357,111 @@ func Decrypt(encryptString string, secret string) (interface{}, error) { } return claims, nil } + +func TestUserHandler_Edit(t *testing.T) { + testCases := []struct { + name string + mock func(ctrl *gomock.Controller) service.UserService + body string + wantCode int + wantBody string + }{ + { + name: "更新成功", + mock: func(ctrl *gomock.Controller) service.UserService { + userSvc := svcmocks.NewMockUserService(ctrl) + userSvc.EXPECT().EditUserProfile(gomock.Any(), gomock.Any()).Return(nil) + return userSvc + }, + body: `{"id":1,"nickname":"frankiejun","birthday":"2020-01-01","aboutme":"I am a good boy"}`, + wantCode: http.StatusOK, + wantBody: "更新成功", + }, + { + name: "数据绑定有问题", + mock: func(ctrl *gomock.Controller) service.UserService { + userSvc := svcmocks.NewMockUserService(ctrl) + return userSvc + }, + body: `{"id":1,"nickname":"frankiejun","birthday":"2020-01-01","aboutme":"I am a good boy"`, + wantCode: http.StatusBadRequest, + }, + { + name: "昵称超长", + mock: func(ctrl *gomock.Controller) service.UserService { + userSvc := svcmocks.NewMockUserService(ctrl) + return userSvc + }, + body: ` +{"id":1, +"nickname":"frankiejun11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111112222222222222222222222222222222222222222222222222222222222222", +"birthday":"2020-01-01", +"about_me":"I am a good boy"} +`, + wantCode: http.StatusOK, + wantBody: "昵称超过长度限制!", + }, + { + name: "个人介绍超长", + mock: func(ctrl *gomock.Controller) service.UserService { + userSvc := svcmocks.NewMockUserService(ctrl) + return userSvc + }, + body: ` +{"id":1, +"nickname":"frankiejun", +"birthday":"2020-01-01", +"aboutme":"I am a good boy5555555555556666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666"} +`, + wantCode: http.StatusOK, + wantBody: "简介超过长度限制!", + }, + { + name: "生日日期非法", + mock: func(ctrl *gomock.Controller) service.UserService { + userSvc := svcmocks.NewMockUserService(ctrl) + return userSvc + }, + body: `{"id":1,"nickname":"frankiejun","birthday":"2020.01.01","aboutme":"I am a good boy"}`, + wantCode: http.StatusOK, + wantBody: "非法的生日日期,标准样式为:yyyy-mm-dd", + }, + { + name: "更新失败!", + mock: func(ctrl *gomock.Controller) service.UserService { + userSvc := svcmocks.NewMockUserService(ctrl) + userSvc.EXPECT().EditUserProfile(gomock.Any(), domain.User{ + Id: 1, + NickName: "frankiejun", + Birthday: "2020-01-01", + AboutMe: "I am a good boy", + }).Return(errors.New("更新失败")) + return userSvc + }, + body: `{"id":1,"nickname":"frankiejun","birthday":"2020-01-01","aboutme":"I am a good boy"}`, + wantCode: http.StatusOK, + wantBody: "更新失败!", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + r := gin.Default() + h := NewUserHandler(tc.mock(ctrl)) + + h.RegisterRoutes(r) + + req, err := http.NewRequest(http.MethodPost, "/users/edit", bytes.NewBuffer([]byte(tc.body))) + + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() + r.ServeHTTP(resp, req) + + assert.Equal(t, tc.wantCode, resp.Code) + assert.Equal(t, tc.wantBody, resp.Body.String()) + }) + } +}