package pub import ( "context" "database/sql" "errors" "testing" "time" permModel "perms-system-server/internal/model/perm" productModel "perms-system-server/internal/model/product" productmemberModel "perms-system-server/internal/model/productmember" userModel "perms-system-server/internal/model/user" "perms-system-server/internal/response" "perms-system-server/internal/svc" "perms-system-server/internal/testutil" "perms-system-server/internal/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func newTestSvcCtx() *svc.ServiceContext { return svc.NewServiceContext(testutil.GetTestConfig()) } func setupCaptcha(t *testing.T) (string, string) { t.Helper() id := "captcha_" + testutil.UniqueId() code := "1234" defaultCaptchaStore.Set(id, code) return id, code } func insertTestUser(t *testing.T, ctx context.Context, svcCtx *svc.ServiceContext, username, password string, status, isSuperAdmin int64) (int64, func()) { t.Helper() conn := testutil.GetTestSqlConn() now := time.Now().Unix() hashed := testutil.HashPassword(password) res, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{ Username: username, Password: hashed, Nickname: username, Avatar: sql.NullString{}, Email: username + "@test.com", Phone: "13800000000", Remark: "", DeptId: 0, IsSuperAdmin: isSuperAdmin, MustChangePassword: 2, Status: status, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) id, _ := res.LastInsertId() cleanup := func() { testutil.CleanTable(ctx, conn, "`sys_user`", id) } return id, cleanup } func insertTestProduct(t *testing.T, ctx context.Context, svcCtx *svc.ServiceContext, code, appKey, appSecret string) (int64, func()) { t.Helper() conn := testutil.GetTestSqlConn() now := time.Now().Unix() res, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{ Code: code, Name: code, AppKey: appKey, AppSecret: appSecret, Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) id, _ := res.LastInsertId() cleanup := func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) } return id, cleanup } // TC-0001: 正常登录(普通用户+productCode) func TestLogin_NormalWithProductCodeBasic(t *testing.T) { ctx := context.Background() svcCtx := newCaptchaDisabledSvcCtx() conn := testutil.GetTestSqlConn() username := testutil.UniqueId() password := "TestPass123" pc := testutil.UniqueId() now := time.Now().Unix() captchaId, captchaCode := setupCaptcha(t) userId, cleanUser := insertTestUser(t, ctx, svcCtx, username, password, 1, 2) t.Cleanup(cleanUser) _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret") t.Cleanup(cleanProduct) pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmemberModel.SysProductMember{ ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) pmId, _ := pmRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId) }) logic := NewLoginLogic(ctx, svcCtx) resp, err := logic.Login(&types.LoginReq{ Username: username, Password: password, ProductCode: pc, CaptchaId: captchaId, CaptchaCode: captchaCode, }) require.NoError(t, err) require.NotNil(t, resp) assert.NotEmpty(t, resp.AccessToken) assert.NotEmpty(t, resp.RefreshToken) assert.True(t, resp.Expires > time.Now().Unix(), "expires应为未来的unix时间戳") assert.Equal(t, username, resp.UserInfo.Username) assert.Equal(t, "MEMBER", resp.UserInfo.MemberType) } // TC-0002: 正常登录-带productCode func TestLogin_NormalWithProductCode(t *testing.T) { ctx := context.Background() svcCtx := newCaptchaDisabledSvcCtx() conn := testutil.GetTestSqlConn() username := testutil.UniqueId() password := "TestPass123" pc := testutil.UniqueId() now := time.Now().Unix() captchaId, captchaCode := setupCaptcha(t) userId, cleanUser := insertTestUser(t, ctx, svcCtx, username, password, 1, 2) t.Cleanup(cleanUser) _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret") t.Cleanup(cleanProduct) pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmemberModel.SysProductMember{ ProductCode: pc, UserId: userId, MemberType: "ADMIN", Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) pmId, _ := pmRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId) }) permRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{ ProductCode: pc, Name: "perm1", Code: testutil.UniqueId(), Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) permId, _ := permRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permId) }) logic := NewLoginLogic(ctx, svcCtx) resp, err := logic.Login(&types.LoginReq{ Username: username, Password: password, ProductCode: pc, CaptchaId: captchaId, CaptchaCode: captchaCode, }) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, "ADMIN", resp.UserInfo.MemberType) assert.NotEmpty(t, resp.UserInfo.Perms) } // TC-0003: 超管通过产品端登录被拒绝 func TestLogin_SuperAdminRejected(t *testing.T) { ctx := context.Background() svcCtx := newCaptchaDisabledSvcCtx() username := testutil.UniqueId() password := "TestPass123" pc := testutil.UniqueId() captchaId, captchaCode := setupCaptcha(t) _, cleanUser := insertTestUser(t, ctx, svcCtx, username, password, 1, 1) t.Cleanup(cleanUser) _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret") t.Cleanup(cleanProduct) logic := NewLoginLogic(ctx, svcCtx) resp, err := logic.Login(&types.LoginReq{ Username: username, Password: password, ProductCode: pc, CaptchaId: captchaId, CaptchaCode: captchaCode, }) require.Nil(t, resp) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 403, codeErr.Code()) assert.Equal(t, "超级管理员不允许通过产品端登录,请使用管理后台", codeErr.Error()) } // TC-0004: 超管无productCode被拒绝 func TestLogin_SuperAdminWithoutProductCodeRejected(t *testing.T) { ctx := context.Background() svcCtx := newCaptchaDisabledSvcCtx() username := testutil.UniqueId() password := "TestPass123" captchaId, captchaCode := setupCaptcha(t) _, cleanUser := insertTestUser(t, ctx, svcCtx, username, password, 1, 1) t.Cleanup(cleanUser) logic := NewLoginLogic(ctx, svcCtx) resp, err := logic.Login(&types.LoginReq{ Username: username, Password: password, CaptchaId: captchaId, CaptchaCode: captchaCode, }) require.Nil(t, resp) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 403, codeErr.Code()) assert.Equal(t, "超级管理员不允许通过产品端登录,请使用管理后台", codeErr.Error()) } // TC-0005: 用户不存在 func TestLogin_UserNotFound(t *testing.T) { ctx := context.Background() svcCtx := newCaptchaDisabledSvcCtx() captchaId, captchaCode := setupCaptcha(t) logic := NewLoginLogic(ctx, svcCtx) resp, err := logic.Login(&types.LoginReq{ Username: "nonexistent_" + testutil.UniqueId(), Password: "whatever", CaptchaId: captchaId, CaptchaCode: captchaCode, }) require.Nil(t, resp) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 401, codeErr.Code()) assert.Equal(t, "用户名或密码错误", codeErr.Error()) } // TC-0007: 密码错误 func TestLogin_WrongPassword(t *testing.T) { ctx := context.Background() svcCtx := newCaptchaDisabledSvcCtx() username := testutil.UniqueId() captchaId, captchaCode := setupCaptcha(t) _, cleanUser := insertTestUser(t, ctx, svcCtx, username, "CorrectPass", 1, 2) t.Cleanup(cleanUser) logic := NewLoginLogic(ctx, svcCtx) resp, err := logic.Login(&types.LoginReq{ Username: username, Password: "WrongPass", CaptchaId: captchaId, CaptchaCode: captchaCode, }) require.Nil(t, resp) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 401, codeErr.Code()) assert.Equal(t, "用户名或密码错误", codeErr.Error()) } // TC-0008: 账号冻结 func TestLogin_AccountFrozen(t *testing.T) { ctx := context.Background() svcCtx := newCaptchaDisabledSvcCtx() username := testutil.UniqueId() password := "TestPass123" captchaId, captchaCode := setupCaptcha(t) _, cleanUser := insertTestUser(t, ctx, svcCtx, username, password, 2, 2) t.Cleanup(cleanUser) logic := NewLoginLogic(ctx, svcCtx) resp, err := logic.Login(&types.LoginReq{ Username: username, Password: password, CaptchaId: captchaId, CaptchaCode: captchaCode, }) require.Nil(t, resp) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 403, codeErr.Code()) assert.Equal(t, "账号已被冻结", codeErr.Error()) } // TC-0009: 非产品成员 func TestLogin_NonMemberWithProductCode(t *testing.T) { ctx := context.Background() svcCtx := newCaptchaDisabledSvcCtx() username := testutil.UniqueId() password := "TestPass123" pc := testutil.UniqueId() captchaId, captchaCode := setupCaptcha(t) _, cleanUser := insertTestUser(t, ctx, svcCtx, username, password, 1, 2) t.Cleanup(cleanUser) _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret") t.Cleanup(cleanProduct) logic := NewLoginLogic(ctx, svcCtx) resp, err := logic.Login(&types.LoginReq{ Username: username, Password: password, ProductCode: pc, CaptchaId: captchaId, CaptchaCode: captchaCode, }) require.Nil(t, resp) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 403, codeErr.Code()) // loginService 去除重复 FindOneByProductCodeUserId,所有非成员/禁用成员分支合并 assert.Equal(t, "您不是该产品的有效成员", codeErr.Error()) } // TC-0010: DEVELOPER成员 func TestLogin_DeveloperMember(t *testing.T) { ctx := context.Background() svcCtx := newCaptchaDisabledSvcCtx() conn := testutil.GetTestSqlConn() username := testutil.UniqueId() password := "TestPass123" pc := testutil.UniqueId() now := time.Now().Unix() captchaId, captchaCode := setupCaptcha(t) userId, cleanUser := insertTestUser(t, ctx, svcCtx, username, password, 1, 2) t.Cleanup(cleanUser) _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret") t.Cleanup(cleanProduct) pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmemberModel.SysProductMember{ ProductCode: pc, UserId: userId, MemberType: "DEVELOPER", Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) pmId, _ := pmRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId) }) permCode := testutil.UniqueId() permRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{ ProductCode: pc, Name: "dev_perm", Code: permCode, Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) permId, _ := permRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permId) }) logic := NewLoginLogic(ctx, svcCtx) resp, err := logic.Login(&types.LoginReq{ Username: username, Password: password, ProductCode: pc, CaptchaId: captchaId, CaptchaCode: captchaCode, }) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, "DEVELOPER", resp.UserInfo.MemberType) assert.Contains(t, resp.UserInfo.Perms, permCode) } // TC-0011: SQL注入 func TestLogin_SQLInjection(t *testing.T) { ctx := context.Background() svcCtx := newCaptchaDisabledSvcCtx() captchaId, captchaCode := setupCaptcha(t) logic := NewLoginLogic(ctx, svcCtx) resp, err := logic.Login(&types.LoginReq{ Username: "' OR 1=1 --", Password: "anything", CaptchaId: captchaId, CaptchaCode: captchaCode, }) require.Nil(t, resp) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 401, codeErr.Code()) assert.Equal(t, "用户名或密码错误", codeErr.Error()) } // TC-0013: 产品成员被禁用时拒绝登录(修复验证) func TestLogin_DisabledMemberRejected(t *testing.T) { ctx := context.Background() svcCtx := newCaptchaDisabledSvcCtx() conn := testutil.GetTestSqlConn() username := testutil.UniqueId() password := "TestPass123" pc := testutil.UniqueId() now := time.Now().Unix() captchaId, captchaCode := setupCaptcha(t) userId, cleanUser := insertTestUser(t, ctx, svcCtx, username, password, 1, 2) t.Cleanup(cleanUser) _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret") t.Cleanup(cleanProduct) pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmemberModel.SysProductMember{ ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 2, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) pmId, _ := pmRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId) }) logic := NewLoginLogic(ctx, svcCtx) resp, err := logic.Login(&types.LoginReq{ Username: username, Password: password, ProductCode: pc, CaptchaId: captchaId, CaptchaCode: captchaCode, }) require.Nil(t, resp) require.Error(t, err) var codeErr2 *response.CodeError require.True(t, errors.As(err, &codeErr2)) assert.Equal(t, 403, codeErr2.Code()) // 禁用成员在 loadMembership 阶段即被清空 MemberType,与"非成员"文案合并 assert.Equal(t, "您不是该产品的有效成员", codeErr2.Error()) } // TC-0014: 产品已被禁用时拒绝登录 func TestLogin_DisabledProductRejected(t *testing.T) { ctx := context.Background() svcCtx := newCaptchaDisabledSvcCtx() conn := testutil.GetTestSqlConn() username := testutil.UniqueId() password := "TestPass123" pc := testutil.UniqueId() now := time.Now().Unix() captchaId, captchaCode := setupCaptcha(t) _, cleanUser := insertTestUser(t, ctx, svcCtx, username, password, 1, 2) t.Cleanup(cleanUser) pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{ Code: pc, Name: pc, AppKey: testutil.UniqueId(), AppSecret: "secret", Status: 2, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) pId, _ := pRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", pId) }) logic := NewLoginLogic(ctx, svcCtx) resp, err := logic.Login(&types.LoginReq{ Username: username, Password: password, ProductCode: pc, CaptchaId: captchaId, CaptchaCode: captchaCode, }) require.Nil(t, resp) require.Error(t, err) var codeErr3 *response.CodeError require.True(t, errors.As(err, &codeErr3)) assert.Equal(t, 403, codeErr3.Code()) assert.Equal(t, "该产品已被禁用", codeErr3.Error()) }