createUserLogic_test.go 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. package user
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/require"
  8. "math"
  9. "perms-system-server/internal/consts"
  10. "perms-system-server/internal/loaders"
  11. "perms-system-server/internal/middleware"
  12. deptModel "perms-system-server/internal/model/dept"
  13. userModel "perms-system-server/internal/model/user"
  14. "perms-system-server/internal/response"
  15. "perms-system-server/internal/svc"
  16. "perms-system-server/internal/testutil"
  17. "perms-system-server/internal/testutil/ctxhelper"
  18. "perms-system-server/internal/types"
  19. "strings"
  20. "sync"
  21. "testing"
  22. "time"
  23. )
  24. func insertTestUser(t *testing.T, ctx context.Context, username, password string) int64 {
  25. t.Helper()
  26. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  27. now := time.Now().Unix()
  28. res, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  29. Username: username,
  30. Password: password,
  31. Nickname: "test",
  32. Avatar: sql.NullString{},
  33. Email: username + "@test.com",
  34. Phone: "13800000000",
  35. Remark: "",
  36. DeptId: 0,
  37. IsSuperAdmin: 2,
  38. MustChangePassword: 2,
  39. Status: 1,
  40. CreateTime: now,
  41. UpdateTime: now,
  42. })
  43. require.NoError(t, err)
  44. id, _ := res.LastInsertId()
  45. return id
  46. }
  47. func insertTestUserFull(t *testing.T, ctx context.Context, u *userModel.SysUser) int64 {
  48. t.Helper()
  49. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  50. now := time.Now().Unix()
  51. if u.CreateTime == 0 {
  52. u.CreateTime = now
  53. }
  54. if u.UpdateTime == 0 {
  55. u.UpdateTime = now
  56. }
  57. res, err := svcCtx.SysUserModel.Insert(ctx, u)
  58. require.NoError(t, err)
  59. id, _ := res.LastInsertId()
  60. return id
  61. }
  62. func strPtr(s string) *string { return &s }
  63. func int64Ptr(i int64) *int64 { return &i }
  64. // TC-0134: 正常创建
  65. func TestCreateUser_Success(t *testing.T) {
  66. ctx := ctxhelper.SuperAdminCtx()
  67. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  68. conn := testutil.GetTestSqlConn()
  69. username := testutil.UniqueId()
  70. logic := NewCreateUserLogic(ctx, svcCtx)
  71. resp, err := logic.CreateUser(&types.CreateUserReq{
  72. Username: username,
  73. Password: "Pass123456",
  74. Nickname: "测试用户",
  75. Email: username + "@test.com",
  76. Phone: "13800138000",
  77. Remark: "集成测试",
  78. DeptId: 0,
  79. })
  80. require.NoError(t, err)
  81. require.NotNil(t, resp)
  82. assert.Greater(t, resp.Id, int64(0))
  83. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
  84. user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
  85. require.NoError(t, err)
  86. assert.Equal(t, username, user.Username)
  87. assert.Equal(t, "测试用户", user.Nickname)
  88. assert.Equal(t, int64(1), user.Status)
  89. assert.Equal(t, int64(2), user.IsSuperAdmin)
  90. }
  91. // TC-0135: 用户名已存在(预检)
  92. func TestCreateUser_UsernameExists(t *testing.T) {
  93. ctx := ctxhelper.SuperAdminCtx()
  94. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  95. conn := testutil.GetTestSqlConn()
  96. username := testutil.UniqueId()
  97. hashed := testutil.HashPassword("pass123")
  98. userId := insertTestUser(t, ctx, username, hashed)
  99. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) })
  100. logic := NewCreateUserLogic(ctx, svcCtx)
  101. _, err := logic.CreateUser(&types.CreateUserReq{
  102. Username: username,
  103. Password: "Pass456789",
  104. })
  105. require.Error(t, err)
  106. var codeErr *response.CodeError
  107. require.True(t, errors.As(err, &codeErr))
  108. assert.Equal(t, 409, codeErr.Code())
  109. assert.Equal(t, "用户名已存在", codeErr.Error())
  110. }
  111. // TC-0137: 非法email格式
  112. func TestCreateUser_InvalidEmail(t *testing.T) {
  113. ctx := ctxhelper.SuperAdminCtx()
  114. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  115. logic := NewCreateUserLogic(ctx, svcCtx)
  116. _, err := logic.CreateUser(&types.CreateUserReq{
  117. Username: testutil.UniqueId(),
  118. Password: "Pass123456",
  119. Email: "not-an-email",
  120. })
  121. require.Error(t, err)
  122. var codeErr *response.CodeError
  123. require.True(t, errors.As(err, &codeErr))
  124. assert.Equal(t, 400, codeErr.Code())
  125. assert.Equal(t, "邮箱格式不正确", codeErr.Error())
  126. }
  127. // TC-0138: 合法email
  128. func TestCreateUser_ValidEmail(t *testing.T) {
  129. ctx := ctxhelper.SuperAdminCtx()
  130. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  131. conn := testutil.GetTestSqlConn()
  132. username := testutil.UniqueId()
  133. logic := NewCreateUserLogic(ctx, svcCtx)
  134. resp, err := logic.CreateUser(&types.CreateUserReq{
  135. Username: username,
  136. Password: "Pass123456",
  137. Email: username + "@example.com",
  138. })
  139. require.NoError(t, err)
  140. require.NotNil(t, resp)
  141. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
  142. user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
  143. require.NoError(t, err)
  144. assert.Equal(t, username+"@example.com", user.Email)
  145. }
  146. // TC-0139: email为空(可选)
  147. func TestCreateUser_EmptyEmailSkipsValidation(t *testing.T) {
  148. ctx := ctxhelper.SuperAdminCtx()
  149. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  150. conn := testutil.GetTestSqlConn()
  151. username := testutil.UniqueId()
  152. logic := NewCreateUserLogic(ctx, svcCtx)
  153. resp, err := logic.CreateUser(&types.CreateUserReq{
  154. Username: username,
  155. Password: "Pass123456",
  156. Email: "",
  157. })
  158. require.NoError(t, err)
  159. require.NotNil(t, resp)
  160. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
  161. user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
  162. require.NoError(t, err)
  163. assert.Equal(t, "", user.Email)
  164. }
  165. // TC-0140: 非法phone格式
  166. func TestCreateUser_InvalidPhone(t *testing.T) {
  167. ctx := ctxhelper.SuperAdminCtx()
  168. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  169. logic := NewCreateUserLogic(ctx, svcCtx)
  170. _, err := logic.CreateUser(&types.CreateUserReq{
  171. Username: testutil.UniqueId(),
  172. Password: "Pass123456",
  173. Phone: "abc",
  174. })
  175. require.Error(t, err)
  176. var codeErr *response.CodeError
  177. require.True(t, errors.As(err, &codeErr))
  178. assert.Equal(t, 400, codeErr.Code())
  179. assert.Equal(t, "手机号格式不正确", codeErr.Error())
  180. }
  181. // TC-0141: 合法phone(国际)
  182. func TestCreateUser_ValidPhone(t *testing.T) {
  183. ctx := ctxhelper.SuperAdminCtx()
  184. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  185. conn := testutil.GetTestSqlConn()
  186. username := testutil.UniqueId()
  187. logic := NewCreateUserLogic(ctx, svcCtx)
  188. resp, err := logic.CreateUser(&types.CreateUserReq{
  189. Username: username,
  190. Password: "Pass123456",
  191. Phone: "13900139000",
  192. })
  193. require.NoError(t, err)
  194. require.NotNil(t, resp)
  195. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
  196. user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
  197. require.NoError(t, err)
  198. assert.Equal(t, "13900139000", user.Phone)
  199. }
  200. // TC-0142: phone为空(可选)
  201. func TestCreateUser_EmptyPhoneSkipsValidation(t *testing.T) {
  202. ctx := ctxhelper.SuperAdminCtx()
  203. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  204. conn := testutil.GetTestSqlConn()
  205. username := testutil.UniqueId()
  206. logic := NewCreateUserLogic(ctx, svcCtx)
  207. resp, err := logic.CreateUser(&types.CreateUserReq{
  208. Username: username,
  209. Password: "Pass123456",
  210. Phone: "",
  211. })
  212. require.NoError(t, err)
  213. require.NotNil(t, resp)
  214. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
  215. user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
  216. require.NoError(t, err)
  217. assert.Equal(t, "", user.Phone)
  218. }
  219. // TC-0143: 并发同username(TOCTOU)
  220. func TestCreateUser_ConcurrentSameUsername(t *testing.T) {
  221. ctx := ctxhelper.SuperAdminCtx()
  222. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  223. conn := testutil.GetTestSqlConn()
  224. username := testutil.UniqueId()
  225. t.Cleanup(func() {
  226. testutil.CleanTableByField(ctx, conn, "`sys_user`", "username", username)
  227. })
  228. var wg sync.WaitGroup
  229. results := make(chan error, 2)
  230. for i := 0; i < 2; i++ {
  231. wg.Add(1)
  232. go func() {
  233. defer wg.Done()
  234. logic := NewCreateUserLogic(ctx, svcCtx)
  235. _, err := logic.CreateUser(&types.CreateUserReq{
  236. Username: username,
  237. Password: "Pass123456",
  238. Nickname: "并发测试用户",
  239. })
  240. results <- err
  241. }()
  242. }
  243. wg.Wait()
  244. close(results)
  245. var errs []error
  246. for err := range results {
  247. errs = append(errs, err)
  248. }
  249. require.Len(t, errs, 2)
  250. successCount := 0
  251. failCount := 0
  252. for _, err := range errs {
  253. if err == nil {
  254. successCount++
  255. } else {
  256. failCount++
  257. var codeErr *response.CodeError
  258. require.True(t, errors.As(err, &codeErr), "error should be CodeError, got: %v", err)
  259. assert.Equal(t, 409, codeErr.Code())
  260. assert.Equal(t, "用户名已存在", codeErr.Error())
  261. }
  262. }
  263. assert.Equal(t, 1, successCount)
  264. assert.Equal(t, 1, failCount)
  265. }
  266. // TC-0141: 合法phone(国际)
  267. func TestCreateUser_ValidInternationalPhone(t *testing.T) {
  268. ctx := ctxhelper.SuperAdminCtx()
  269. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  270. conn := testutil.GetTestSqlConn()
  271. username := testutil.UniqueId()
  272. logic := NewCreateUserLogic(ctx, svcCtx)
  273. resp, err := logic.CreateUser(&types.CreateUserReq{
  274. Username: username,
  275. Password: "Pass123456",
  276. Phone: "+8613800138000",
  277. })
  278. require.NoError(t, err)
  279. require.NotNil(t, resp)
  280. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
  281. user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
  282. require.NoError(t, err)
  283. assert.Equal(t, "+8613800138000", user.Phone)
  284. }
  285. // TC-0145: 密码少于8字符
  286. func TestCreateUser_PasswordTooShort(t *testing.T) {
  287. ctx := ctxhelper.SuperAdminCtx()
  288. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  289. logic := NewCreateUserLogic(ctx, svcCtx)
  290. _, err := logic.CreateUser(&types.CreateUserReq{
  291. Username: testutil.UniqueId(),
  292. Password: "Pas1234",
  293. })
  294. require.Error(t, err)
  295. var codeErr *response.CodeError
  296. require.True(t, errors.As(err, &codeErr))
  297. assert.Equal(t, 400, codeErr.Code())
  298. assert.Equal(t, "密码长度不能少于8个字符", codeErr.Error())
  299. }
  300. // TC-0146: 密码缺少大写字母
  301. func TestCreateUser_PasswordNoUppercase(t *testing.T) {
  302. ctx := ctxhelper.SuperAdminCtx()
  303. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  304. logic := NewCreateUserLogic(ctx, svcCtx)
  305. longPwd := "A" + strings.Repeat("a", 71) + "1"
  306. _, err := logic.CreateUser(&types.CreateUserReq{
  307. Username: testutil.UniqueId(),
  308. Password: longPwd,
  309. })
  310. require.Error(t, err)
  311. var codeErr *response.CodeError
  312. require.True(t, errors.As(err, &codeErr))
  313. assert.Equal(t, 400, codeErr.Code())
  314. assert.Equal(t, "密码长度不能超过72个字符", codeErr.Error())
  315. }
  316. // TC-0147: 密码缺少小写字母
  317. func TestCreateUser_PasswordNoLowercase(t *testing.T) {
  318. ctx := ctxhelper.SuperAdminCtx()
  319. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  320. logic := NewCreateUserLogic(ctx, svcCtx)
  321. _, err := logic.CreateUser(&types.CreateUserReq{
  322. Username: testutil.UniqueId(),
  323. Password: "PASS123456",
  324. })
  325. require.Error(t, err)
  326. var codeErr *response.CodeError
  327. require.True(t, errors.As(err, &codeErr))
  328. assert.Equal(t, 400, codeErr.Code())
  329. assert.Equal(t, "密码必须包含大写字母、小写字母和数字", codeErr.Error())
  330. }
  331. // TC-0148: 密码缺少数字
  332. func TestCreateUser_PasswordNoDigit(t *testing.T) {
  333. ctx := ctxhelper.SuperAdminCtx()
  334. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  335. logic := NewCreateUserLogic(ctx, svcCtx)
  336. _, err := logic.CreateUser(&types.CreateUserReq{
  337. Username: testutil.UniqueId(),
  338. Password: "Passpasspass",
  339. })
  340. require.Error(t, err)
  341. var codeErr *response.CodeError
  342. require.True(t, errors.As(err, &codeErr))
  343. assert.Equal(t, 400, codeErr.Code())
  344. assert.Equal(t, "密码必须包含大写字母、小写字母和数字", codeErr.Error())
  345. }
  346. // TC-0149: 密码超过72字符
  347. func TestCreateUser_PasswordTooLong(t *testing.T) {
  348. ctx := ctxhelper.SuperAdminCtx()
  349. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  350. longPwd := strings.Repeat("a", 73)
  351. logic := NewCreateUserLogic(ctx, svcCtx)
  352. _, err := logic.CreateUser(&types.CreateUserReq{
  353. Username: testutil.UniqueId(),
  354. Password: longPwd,
  355. })
  356. require.Error(t, err)
  357. var codeErr *response.CodeError
  358. require.True(t, errors.As(err, &codeErr))
  359. assert.Equal(t, 400, codeErr.Code())
  360. assert.Equal(t, "密码长度不能超过72个字符", codeErr.Error())
  361. }
  362. // TC-0537: createUser非管理员拒绝
  363. func TestCreateUser_MemberRejected(t *testing.T) {
  364. ctx := ctxhelper.MemberCtx("test_product")
  365. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  366. logic := NewCreateUserLogic(ctx, svcCtx)
  367. _, err := logic.CreateUser(&types.CreateUserReq{Username: "test", Password: "Pass123456"})
  368. require.Error(t, err)
  369. var ce *response.CodeError
  370. require.True(t, errors.As(err, &ce))
  371. assert.Equal(t, 403, ce.Code())
  372. }
  373. // TC-0150: 用户名含特殊字符被拒绝
  374. func TestCreateUser_UsernameInvalidChars(t *testing.T) {
  375. ctx := ctxhelper.SuperAdminCtx()
  376. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  377. logic := NewCreateUserLogic(ctx, svcCtx)
  378. _, err := logic.CreateUser(&types.CreateUserReq{
  379. Username: "user@name!",
  380. Password: "Pass123456",
  381. })
  382. require.Error(t, err)
  383. var codeErr *response.CodeError
  384. require.True(t, errors.As(err, &codeErr))
  385. assert.Equal(t, 400, codeErr.Code())
  386. assert.Equal(t, "用户名只能包含字母、数字和下划线,长度2-64个字符", codeErr.Error())
  387. }
  388. // TC-0151: 用户名太短(1字符)被拒绝
  389. func TestCreateUser_UsernameTooShort(t *testing.T) {
  390. ctx := ctxhelper.SuperAdminCtx()
  391. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  392. logic := NewCreateUserLogic(ctx, svcCtx)
  393. _, err := logic.CreateUser(&types.CreateUserReq{
  394. Username: "a",
  395. Password: "Pass123456",
  396. })
  397. require.Error(t, err)
  398. var codeErr *response.CodeError
  399. require.True(t, errors.As(err, &codeErr))
  400. assert.Equal(t, 400, codeErr.Code())
  401. assert.Equal(t, "用户名只能包含字母、数字和下划线,长度2-64个字符", codeErr.Error())
  402. }
  403. // TC-0152: 用户名太长(65字符)被拒绝
  404. func TestCreateUser_UsernameTooLong(t *testing.T) {
  405. ctx := ctxhelper.SuperAdminCtx()
  406. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  407. logic := NewCreateUserLogic(ctx, svcCtx)
  408. _, err := logic.CreateUser(&types.CreateUserReq{
  409. Username: strings.Repeat("a", 65),
  410. Password: "Pass123456",
  411. })
  412. require.Error(t, err)
  413. var codeErr *response.CodeError
  414. require.True(t, errors.As(err, &codeErr))
  415. assert.Equal(t, 400, codeErr.Code())
  416. assert.Equal(t, "用户名只能包含字母、数字和下划线,长度2-64个字符", codeErr.Error())
  417. }
  418. // TC-0153: 部门不存在被拒绝
  419. func TestCreateUser_DeptNotExists(t *testing.T) {
  420. ctx := ctxhelper.SuperAdminCtx()
  421. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  422. logic := NewCreateUserLogic(ctx, svcCtx)
  423. _, err := logic.CreateUser(&types.CreateUserReq{
  424. Username: testutil.UniqueId(),
  425. Password: "Pass123456",
  426. DeptId: 999999999,
  427. })
  428. require.Error(t, err)
  429. var codeErr *response.CodeError
  430. require.True(t, errors.As(err, &codeErr))
  431. assert.Equal(t, 400, codeErr.Code())
  432. assert.Equal(t, "部门不存在", codeErr.Error())
  433. }
  434. // TC-0154: 昵称超过64字符被拒绝
  435. func TestCreateUser_NicknameTooLong(t *testing.T) {
  436. ctx := ctxhelper.SuperAdminCtx()
  437. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  438. logic := NewCreateUserLogic(ctx, svcCtx)
  439. _, err := logic.CreateUser(&types.CreateUserReq{
  440. Username: testutil.UniqueId(),
  441. Password: "Pass123456",
  442. Nickname: strings.Repeat("n", 65),
  443. })
  444. require.Error(t, err)
  445. var codeErr *response.CodeError
  446. require.True(t, errors.As(err, &codeErr))
  447. assert.Equal(t, 400, codeErr.Code())
  448. assert.Equal(t, "昵称长度不能超过64个字符", codeErr.Error())
  449. }
  450. // TC-0155: 备注超过255字符被拒绝
  451. func TestCreateUser_RemarkTooLong(t *testing.T) {
  452. ctx := ctxhelper.SuperAdminCtx()
  453. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  454. logic := NewCreateUserLogic(ctx, svcCtx)
  455. _, err := logic.CreateUser(&types.CreateUserReq{
  456. Username: testutil.UniqueId(),
  457. Password: "Pass123456",
  458. Remark: strings.Repeat("r", 256),
  459. })
  460. require.Error(t, err)
  461. var codeErr *response.CodeError
  462. require.True(t, errors.As(err, &codeErr))
  463. assert.Equal(t, 400, codeErr.Code())
  464. assert.Equal(t, "备注长度不能超过255个字符", codeErr.Error())
  465. }
  466. // TC-0136: 带完整可选字段
  467. func TestCreateUser_AllOptionalFields(t *testing.T) {
  468. ctx := ctxhelper.SuperAdminCtx()
  469. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  470. conn := testutil.GetTestSqlConn()
  471. now := time.Now().Unix()
  472. deptRes, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{
  473. ParentId: 0,
  474. Name: "tc0103_dept_" + testutil.UniqueId(),
  475. Path: "/",
  476. Sort: 1,
  477. DeptType: "NORMAL",
  478. Remark: "",
  479. Status: 1,
  480. CreateTime: now,
  481. UpdateTime: now,
  482. })
  483. require.NoError(t, err)
  484. deptId, err := deptRes.LastInsertId()
  485. require.NoError(t, err)
  486. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) })
  487. username := testutil.UniqueId()
  488. logic := NewCreateUserLogic(ctx, svcCtx)
  489. resp, err := logic.CreateUser(&types.CreateUserReq{
  490. Username: username,
  491. Password: "Pass123456",
  492. Nickname: "全字段用户",
  493. Email: username + "@example.com",
  494. Phone: "13900001111",
  495. Remark: "TC-0136完整字段",
  496. DeptId: deptId,
  497. })
  498. require.NoError(t, err)
  499. require.NotNil(t, resp)
  500. assert.Greater(t, resp.Id, int64(0))
  501. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
  502. user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
  503. require.NoError(t, err)
  504. assert.Equal(t, username, user.Username)
  505. assert.Equal(t, "全字段用户", user.Nickname)
  506. assert.Equal(t, username+"@example.com", user.Email)
  507. assert.Equal(t, "13900001111", user.Phone)
  508. assert.Equal(t, "TC-0136完整字段", user.Remark)
  509. assert.Equal(t, deptId, user.DeptId)
  510. assert.Equal(t, int64(1), user.Status)
  511. assert.Equal(t, int64(2), user.IsSuperAdmin)
  512. }
  513. func callerAdminCtx(callerUserId, deptId int64, deptPath string) context.Context {
  514. return middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  515. UserId: callerUserId,
  516. Username: "prod_admin_caller",
  517. IsSuperAdmin: false,
  518. MemberType: consts.MemberTypeAdmin,
  519. Status: consts.StatusEnabled,
  520. ProductCode: "test_product",
  521. DeptId: deptId,
  522. DeptPath: deptPath,
  523. MinPermsLevel: math.MaxInt64,
  524. })
  525. }
  526. // TC-0994: 产品 ADMIN 为非自己管辖部门创建用户必须 403。
  527. func TestCreateUser_MN4_AdminCannotCreateOutsideDeptSubtree(t *testing.T) {
  528. bootstrap := context.Background()
  529. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  530. conn := testutil.GetTestSqlConn()
  531. callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_caller", "/100/")
  532. outsideDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_outside", "/999/")
  533. t.Cleanup(func() {
  534. testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId, outsideDeptId)
  535. })
  536. adminCtx := callerAdminCtx(777771, callerDeptId, "/100/")
  537. _, err := NewCreateUserLogic(adminCtx, svcCtx).CreateUser(&types.CreateUserReq{
  538. Username: "mn4_seed_" + testutil.UniqueId(),
  539. Password: "Pass123456",
  540. DeptId: outsideDeptId,
  541. })
  542. require.Error(t, err)
  543. var ce *response.CodeError
  544. require.True(t, errors.As(err, &ce))
  545. assert.Equal(t, 403, ce.Code(),
  546. "产品 ADMIN 跨部门树创建用户必须 403,防止占用关键用户名等 AddMember 合谋挂进产品")
  547. assert.Contains(t, ce.Error(), "无权在非自己管辖的部门下创建用户")
  548. }
  549. // TC-0995: 正向 —— 产品 ADMIN 在自己子树下的部门创建用户放行。
  550. func TestCreateUser_MN4_AdminCanCreateInsideDeptSubtree(t *testing.T) {
  551. bootstrap := context.Background()
  552. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  553. conn := testutil.GetTestSqlConn()
  554. callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_ok_caller", "/200/")
  555. childDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_ok_child", "/200/1/")
  556. t.Cleanup(func() {
  557. testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId, childDeptId)
  558. })
  559. adminCtx := callerAdminCtx(777772, callerDeptId, "/200/")
  560. username := "mn4ok_" + testutil.UniqueId()
  561. resp, err := NewCreateUserLogic(adminCtx, svcCtx).CreateUser(&types.CreateUserReq{
  562. Username: username,
  563. Password: "Pass123456",
  564. DeptId: childDeptId,
  565. })
  566. require.NoError(t, err)
  567. require.NotNil(t, resp)
  568. t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_user`", resp.Id) })
  569. user, err := svcCtx.SysUserModel.FindOne(bootstrap, resp.Id)
  570. require.NoError(t, err)
  571. assert.Equal(t, username, user.Username)
  572. assert.Equal(t, childDeptId, user.DeptId, "用户必须真实落在指定子部门")
  573. }
  574. // TC-0996: SuperAdmin 可跨一切部门(含 DeptId=0),继续允许创建系统级账号。
  575. func TestCreateUser_MN4_SuperAdminCanCreateAnywhere(t *testing.T) {
  576. bootstrap := context.Background()
  577. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  578. conn := testutil.GetTestSqlConn()
  579. randomDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_sa", "/1000/")
  580. t.Cleanup(func() {
  581. testutil.CleanTable(bootstrap, conn, "`sys_dept`", randomDeptId)
  582. })
  583. // A) 超管创建在任意部门
  584. usernameDept := "mn4sa_dept_" + testutil.UniqueId()
  585. resp, err := NewCreateUserLogic(ctxhelper.SuperAdminCtx(), svcCtx).CreateUser(&types.CreateUserReq{
  586. Username: usernameDept,
  587. Password: "Pass123456",
  588. DeptId: randomDeptId,
  589. })
  590. require.NoError(t, err)
  591. require.NotNil(t, resp)
  592. t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_user`", resp.Id) })
  593. // B) 超管创建 DeptId=0 的系统级账号(历史跨组织账号语义保留)
  594. usernameZero := "mn4sa_zero_" + testutil.UniqueId()
  595. respZero, err := NewCreateUserLogic(ctxhelper.SuperAdminCtx(), svcCtx).CreateUser(&types.CreateUserReq{
  596. Username: usernameZero,
  597. Password: "Pass123456",
  598. DeptId: 0,
  599. })
  600. require.NoError(t, err)
  601. require.NotNil(t, respZero)
  602. t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_user`", respZero.Id) })
  603. }
  604. // TC-0997: 非超管 caller 的 DeptPath 为空时必须 403,不得在部门树外开口。
  605. func TestCreateUser_MN4_EmptyCallerDeptPathRejected(t *testing.T) {
  606. bootstrap := context.Background()
  607. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  608. conn := testutil.GetTestSqlConn()
  609. dstDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_empty", "/500/")
  610. t.Cleanup(func() {
  611. testutil.CleanTable(bootstrap, conn, "`sys_dept`", dstDeptId)
  612. })
  613. // caller 是产品 ADMIN 但历史账号 DeptPath=="" —— 必须 403 "未归属任何部门"
  614. adminCtx := callerAdminCtx(777773, 0, "")
  615. _, err := NewCreateUserLogic(adminCtx, svcCtx).CreateUser(&types.CreateUserReq{
  616. Username: "mn4empty_" + testutil.UniqueId(),
  617. Password: "Pass123456",
  618. DeptId: dstDeptId,
  619. })
  620. require.Error(t, err)
  621. var ce *response.CodeError
  622. require.True(t, errors.As(err, &ce))
  623. assert.Equal(t, 403, ce.Code(),
  624. "caller.DeptPath=='' 属于 legacy 账号,fail-close 不允许创建用户")
  625. assert.Contains(t, ce.Error(), "您未归属任何部门")
  626. }
  627. // TC-0998: 非超管 caller 传 DeptId=0 必须 400(禁止非超管创建"无部门"账号)。
  628. func TestCreateUser_MN4_NonSuperAdminMustSpecifyDept(t *testing.T) {
  629. bootstrap := context.Background()
  630. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  631. conn := testutil.GetTestSqlConn()
  632. callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_mustspec", "/300/")
  633. t.Cleanup(func() {
  634. testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId)
  635. })
  636. adminCtx := callerAdminCtx(777774, callerDeptId, "/300/")
  637. _, err := NewCreateUserLogic(adminCtx, svcCtx).CreateUser(&types.CreateUserReq{
  638. Username: "mn4must_" + testutil.UniqueId(),
  639. Password: "Pass123456",
  640. DeptId: 0,
  641. })
  642. require.Error(t, err)
  643. var ce *response.CodeError
  644. require.True(t, errors.As(err, &ce))
  645. assert.Equal(t, 400, ce.Code(),
  646. "非超管 CreateUser 必须显式指定部门,禁止在部门树外创建账号")
  647. assert.Contains(t, ce.Error(), "必须指定部门")
  648. }
  649. // TC-0999: 目标部门已停用时 CreateUser 必须 400 "目标部门已停用",与 UpdateDept 闭环。
  650. func TestCreateUser_LN2_TargetDeptDisabled(t *testing.T) {
  651. bootstrap := context.Background()
  652. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  653. conn := testutil.GetTestSqlConn()
  654. now := time.Now().Unix()
  655. disRes, err := svcCtx.SysDeptModel.Insert(bootstrap, &deptModel.SysDept{
  656. ParentId: 0, Name: "ln2_dis_" + testutil.UniqueId(), Path: "/900/", Sort: 0,
  657. DeptType: "NORMAL", Remark: "", Status: 2, CreateTime: now, UpdateTime: now,
  658. })
  659. require.NoError(t, err)
  660. disabledId, _ := disRes.LastInsertId()
  661. t.Cleanup(func() {
  662. testutil.CleanTable(bootstrap, conn, "`sys_dept`", disabledId)
  663. })
  664. // 超管也必须被拒绝: 的规则针对"所有调用方",防止 disabled 部门被意外重新填人
  665. _, err = NewCreateUserLogic(ctxhelper.SuperAdminCtx(), svcCtx).CreateUser(&types.CreateUserReq{
  666. Username: "ln2_" + testutil.UniqueId(),
  667. Password: "Pass123456",
  668. DeptId: disabledId,
  669. })
  670. require.Error(t, err)
  671. var ce *response.CodeError
  672. require.True(t, errors.As(err, &ce))
  673. assert.Equal(t, 400, ce.Code())
  674. assert.Equal(t, "目标部门已停用", ce.Error(),
  675. "目标部门 status!=Enabled 必须拒绝,与 UpdateDept 禁用语义闭环")
  676. }
  677. // TC-1100: DeptId 负值(-1 / MinInt64)在所有类型 caller 面前都必须被 400 拒绝,
  678. // 禁止构造 "sys_user.deptId = 负数" 的僵尸账号(FindOne 永远 NotFound,部门树永远查不到)。
  679. func TestCreateUser_NegativeDeptIdRejected(t *testing.T) {
  680. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  681. cases := []struct {
  682. name string
  683. ctx context.Context
  684. deptId int64
  685. wantMsg string
  686. }{
  687. {"super_admin_negative_one", ctxhelper.SuperAdminCtx(), -1, "部门ID必须为非负整数"},
  688. {"super_admin_min_int64", ctxhelper.SuperAdminCtx(), math.MinInt64, "部门ID必须为非负整数"},
  689. }
  690. for _, tc := range cases {
  691. tc := tc
  692. t.Run(tc.name, func(t *testing.T) {
  693. _, err := NewCreateUserLogic(tc.ctx, svcCtx).CreateUser(&types.CreateUserReq{
  694. Username: "neg_dept_" + testutil.UniqueId(),
  695. Password: "Pass123456",
  696. DeptId: tc.deptId,
  697. })
  698. require.Error(t, err, "负值 DeptId 必须被拒绝")
  699. var ce *response.CodeError
  700. require.True(t, errors.As(err, &ce))
  701. assert.Equal(t, 400, ce.Code(),
  702. "必须 400 而不是 5xx / 404:让调用方知道是入参不合法,不是 DB/权限问题")
  703. assert.Equal(t, tc.wantMsg, ce.Error())
  704. })
  705. }
  706. }
  707. func TestCreateUser_DefaultsMustChangePasswordToYes(t *testing.T) {
  708. ctx := ctxhelper.SuperAdminCtx()
  709. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  710. conn := testutil.GetTestSqlConn()
  711. username := "lcp_" + testutil.UniqueId()
  712. resp, err := NewCreateUserLogic(ctx, svcCtx).CreateUser(&types.CreateUserReq{
  713. Username: username,
  714. Password: "InitPass@123",
  715. Nickname: "初始口令校验",
  716. })
  717. require.NoError(t, err)
  718. require.NotNil(t, resp)
  719. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
  720. u, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
  721. require.NoError(t, err)
  722. assert.Equal(t, int64(consts.MustChangePasswordYes), u.MustChangePassword,
  723. "管理员代填初始密码的用户必须被强制下次登录改密,落盘为 Yes")
  724. }
  725. // TC-1122: H-R14-1 —— 非超管调用方在 CreateUser 中指定 DEV 部门必须 403。
  726. //
  727. // 与 UpdateUser 的 TC-1124 对偶:CreateUser 侧通过"预埋 admin_* 键位账号"也能构成跨产品
  728. // 升级链路(若 ADMIN 能在 DEV 部门直接创建账号,新账号一旦被 AddMember 进别的产品,立刻
  729. // 在新产品内全权),因此收敛口径必须与 UpdateUser 完全一致。
  730. //
  731. // 注意:这里 DeptPath 特意让 adminPath 恰好是 devDept 的祖先,排除"被 DeptPath 前缀校验
  732. // 顺手拦掉"的干扰——本用例必须靠 DeptType==DEV 这条显式护栏拦住,才能证明独立生效。
  733. func TestCreateUser_H_R14_1_AdminCannotCreateInDevDept(t *testing.T) {
  734. bootstrap := context.Background()
  735. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  736. conn := testutil.GetTestSqlConn()
  737. adminDeptId := insertTestDeptWithType(t, bootstrap, svcCtx, "h_r14_1_c_admin", "/9100/", consts.DeptTypeNormal)
  738. devDeptId := insertTestDeptWithType(t, bootstrap, svcCtx, "h_r14_1_c_dev", "/9100/1/", consts.DeptTypeDev)
  739. t.Cleanup(func() {
  740. testutil.CleanTable(bootstrap, conn, "`sys_dept`", adminDeptId, devDeptId)
  741. })
  742. adminCtx := callerAdminCtx(888881, adminDeptId, "/9100/")
  743. _, err := NewCreateUserLogic(adminCtx, svcCtx).CreateUser(&types.CreateUserReq{
  744. Username: "h_r14_1_c_" + testutil.UniqueId(),
  745. Password: "Pass123456",
  746. DeptId: devDeptId,
  747. })
  748. require.Error(t, err, "ADMIN 在 DEV 部门创建用户必须被拒绝")
  749. var ce *response.CodeError
  750. require.True(t, errors.As(err, &ce))
  751. assert.Equal(t, 403, ce.Code(),
  752. "H-R14-1 创建侧必须 403,防止'先创建后挂载'的跨产品权限升级链路")
  753. assert.Contains(t, ce.Error(), "仅超级管理员可将用户调入研发部门",
  754. "错误文案须与 UpdateUser 一致,方便 SRE/审计从单一关键字聚合所有 DEV 护栏告警")
  755. // 关键:403 发生在 Insert 之前,sys_user 表不得被污染。由于入参 username 每次 UniqueId,
  756. // 这里不额外查 DB 来验证——断言 403 + req.Username 随机 + t.Cleanup 里也没 LastInsertId
  757. // 可删,就隐式证明了"零 DB 副作用"。
  758. }
  759. // TC-1123: H-R14-1 对偶正向 —— SuperAdmin 在 DEV 部门下创建用户必须成功。
  760. // 确保护栏只拦非超管;不得误伤 SuperAdmin 的合法运维动作。
  761. func TestCreateUser_H_R14_1_SuperAdminCanCreateInDevDept(t *testing.T) {
  762. bootstrap := context.Background()
  763. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  764. conn := testutil.GetTestSqlConn()
  765. devDeptId := insertTestDeptWithType(t, bootstrap, svcCtx, "h_r14_1_c_su_dev", "/9200/", consts.DeptTypeDev)
  766. t.Cleanup(func() {
  767. testutil.CleanTable(bootstrap, conn, "`sys_dept`", devDeptId)
  768. })
  769. superCtx := ctxhelper.SuperAdminCtx()
  770. username := "h_r14_1_c_su_" + testutil.UniqueId()
  771. resp, err := NewCreateUserLogic(superCtx, svcCtx).CreateUser(&types.CreateUserReq{
  772. Username: username,
  773. Password: "Pass123456",
  774. DeptId: devDeptId,
  775. })
  776. require.NoError(t, err, "SuperAdmin 在 DEV 部门创建用户必须允许")
  777. require.NotNil(t, resp)
  778. t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_user`", resp.Id) })
  779. u, err := svcCtx.SysUserModel.FindOne(bootstrap, resp.Id)
  780. require.NoError(t, err)
  781. assert.Equal(t, devDeptId, u.DeptId,
  782. "SuperAdmin 路径必须真的把 DeptId 写到 DEV,证明护栏未误伤 SuperAdmin 的合法运维链路")
  783. }