createProductLogic_test.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. package product
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "sync"
  6. "testing"
  7. productModel "perms-system-server/internal/model/product"
  8. "perms-system-server/internal/response"
  9. "perms-system-server/internal/svc"
  10. "perms-system-server/internal/testutil"
  11. "perms-system-server/internal/testutil/ctxhelper"
  12. "perms-system-server/internal/types"
  13. "github.com/stretchr/testify/assert"
  14. "github.com/stretchr/testify/require"
  15. "golang.org/x/crypto/bcrypt"
  16. )
  17. // TC-0064: 正常创建
  18. func TestCreateProduct_Success(t *testing.T) {
  19. ctx := ctxhelper.SuperAdminCtx()
  20. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  21. conn := testutil.GetTestSqlConn()
  22. code := testutil.UniqueId()
  23. logic := NewCreateProductLogic(ctx, svcCtx)
  24. resp, err := logic.CreateProduct(&types.CreateProductReq{
  25. Code: code,
  26. Name: "测试产品",
  27. Remark: "集成测试",
  28. AdminDeptId: seedAdminDept(t, ctx, svcCtx),
  29. })
  30. require.NoError(t, err)
  31. require.NotNil(t, resp)
  32. t.Cleanup(func() {
  33. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
  34. testutil.CleanTableByField(ctx, conn, "`sys_user`", "username", "admin_"+code)
  35. testutil.CleanTable(ctx, conn, "`sys_product`", resp.Id)
  36. })
  37. assert.True(t, resp.Id > 0)
  38. assert.Equal(t, code, resp.Code)
  39. assert.NotEmpty(t, resp.AppKey)
  40. assert.Equal(t, "admin_"+code, resp.AdminUser)
  41. // 审计 M-4:响应体必须不再明文携带 appSecret / adminPassword,
  42. // 改为发放一次性 credentialsTicket + 过期时间;调用方需凭 ticket 走
  43. // /api/product/fetchInitialCredentials 领取敏感凭证。
  44. assert.NotEmpty(t, resp.CredentialsTicket, "M-4:必须返回一次性凭证票据")
  45. assert.True(t, resp.CredentialsExpiresAt > 0, "M-4:必须返回过期时间戳")
  46. // 契约性校验:CreateProductResp 的 JSON 序列化里不应再出现 appSecret / adminPassword 字段。
  47. buf, err := json.Marshal(resp)
  48. require.NoError(t, err)
  49. var asMap map[string]interface{}
  50. require.NoError(t, json.Unmarshal(buf, &asMap))
  51. _, hasSecret := asMap["appSecret"]
  52. _, hasPwd := asMap["adminPassword"]
  53. assert.False(t, hasSecret, "M-4:CreateProductResp JSON 不得包含 appSecret 字段(避免日志落盘)")
  54. assert.False(t, hasPwd, "M-4:CreateProductResp JSON 不得包含 adminPassword 字段(避免日志落盘)")
  55. }
  56. // TC-0064: 正常创建
  57. func TestCreateProduct_VerifyDB(t *testing.T) {
  58. ctx := ctxhelper.SuperAdminCtx()
  59. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  60. conn := testutil.GetTestSqlConn()
  61. code := testutil.UniqueId()
  62. logic := NewCreateProductLogic(ctx, svcCtx)
  63. resp, err := logic.CreateProduct(&types.CreateProductReq{
  64. Code: code,
  65. Name: "DB验证产品",
  66. Remark: "验证数据库记录",
  67. AdminDeptId: seedAdminDept(t, ctx, svcCtx),
  68. })
  69. require.NoError(t, err)
  70. t.Cleanup(func() {
  71. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
  72. testutil.CleanTableByField(ctx, conn, "`sys_user`", "username", "admin_"+code)
  73. testutil.CleanTable(ctx, conn, "`sys_product`", resp.Id)
  74. })
  75. product, err := svcCtx.SysProductModel.FindOne(ctx, resp.Id)
  76. require.NoError(t, err)
  77. assert.Equal(t, code, product.Code)
  78. assert.Equal(t, "DB验证产品", product.Name)
  79. assert.Equal(t, resp.AppKey, product.AppKey)
  80. // 审计 M-4:CreateProduct 响应不再明文吐 appSecret;appSecret 经 ticket 领取后再核对。
  81. // 这里改为用 FetchInitialCredentialsLogic 把明文 appSecret 取出来,与 DB 中的 bcrypt hash 比对,
  82. // 既验证"DB 存的是 hash 而不是明文",也验证 ticket 流程正确交还了原始 appSecret。
  83. fetch := NewFetchInitialCredentialsLogic(ctx, svcCtx)
  84. cred, err := fetch.FetchInitialCredentials(&types.FetchInitialCredentialsReq{Ticket: resp.CredentialsTicket})
  85. require.NoError(t, err, "M-4:使用 ticket 必须能领取到初始 appSecret / adminPassword")
  86. require.NotEmpty(t, cred.AppSecret)
  87. require.NotEmpty(t, cred.AdminPassword)
  88. assert.NoError(t, bcrypt.CompareHashAndPassword([]byte(product.AppSecret), []byte(cred.AppSecret)),
  89. "DB should store bcrypt hash of appSecret, verifiable with plaintext from ticket payload")
  90. assert.Equal(t, int64(1), product.Status)
  91. var userCount int64
  92. err = conn.QueryRowCtx(ctx, &userCount,
  93. "SELECT COUNT(*) FROM `sys_user` WHERE `username` = ?", "admin_"+code)
  94. require.NoError(t, err)
  95. assert.Equal(t, int64(1), userCount)
  96. var memberCount int64
  97. err = conn.QueryRowCtx(ctx, &memberCount,
  98. "SELECT COUNT(*) FROM `sys_product_member` WHERE `productCode` = ? AND `memberType` = 'ADMIN'", code)
  99. require.NoError(t, err)
  100. assert.Equal(t, int64(1), memberCount)
  101. }
  102. // TC-0067: 编码已存在
  103. func TestCreateProduct_DuplicateCode(t *testing.T) {
  104. ctx := ctxhelper.SuperAdminCtx()
  105. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  106. conn := testutil.GetTestSqlConn()
  107. code := testutil.UniqueId()
  108. deptId := seedAdminDept(t, ctx, svcCtx)
  109. logic := NewCreateProductLogic(ctx, svcCtx)
  110. resp, err := logic.CreateProduct(&types.CreateProductReq{
  111. Code: code,
  112. Name: "第一个产品",
  113. AdminDeptId: deptId,
  114. })
  115. require.NoError(t, err)
  116. t.Cleanup(func() {
  117. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
  118. testutil.CleanTableByField(ctx, conn, "`sys_user`", "username", "admin_"+code)
  119. testutil.CleanTable(ctx, conn, "`sys_product`", resp.Id)
  120. })
  121. logic2 := NewCreateProductLogic(ctx, svcCtx)
  122. _, err = logic2.CreateProduct(&types.CreateProductReq{
  123. Code: code,
  124. Name: "重复产品",
  125. AdminDeptId: deptId,
  126. })
  127. require.Error(t, err)
  128. var codeErr *response.CodeError
  129. require.True(t, errors.As(err, &codeErr))
  130. assert.Equal(t, 409, codeErr.Code())
  131. assert.Equal(t, "产品编码已存在", codeErr.Error())
  132. }
  133. // TC-0068: 并发创建同编码
  134. func TestCreateProduct_ConcurrentSameCode(t *testing.T) {
  135. ctx := ctxhelper.SuperAdminCtx()
  136. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  137. conn := testutil.GetTestSqlConn()
  138. code := testutil.UniqueId()
  139. t.Cleanup(func() {
  140. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
  141. testutil.CleanTableByField(ctx, conn, "`sys_user`", "username", "admin_"+code)
  142. testutil.CleanTableByField(ctx, conn, "`sys_product`", "code", code)
  143. })
  144. deptId := seedAdminDept(t, ctx, svcCtx)
  145. var wg sync.WaitGroup
  146. results := make(chan error, 2)
  147. for i := 0; i < 2; i++ {
  148. wg.Add(1)
  149. go func() {
  150. defer wg.Done()
  151. logic := NewCreateProductLogic(ctx, svcCtx)
  152. _, err := logic.CreateProduct(&types.CreateProductReq{
  153. Code: code,
  154. Name: "并发测试产品",
  155. AdminDeptId: deptId,
  156. })
  157. results <- err
  158. }()
  159. }
  160. wg.Wait()
  161. close(results)
  162. var errs []error
  163. for err := range results {
  164. errs = append(errs, err)
  165. }
  166. require.Len(t, errs, 2)
  167. successCount := 0
  168. failCount := 0
  169. for _, err := range errs {
  170. if err == nil {
  171. successCount++
  172. } else {
  173. failCount++
  174. }
  175. }
  176. assert.Equal(t, 1, successCount, "exactly one goroutine should succeed")
  177. assert.Equal(t, 1, failCount, "exactly one goroutine should fail (409 or DB duplicate)")
  178. }
  179. // TC-0535: createProduct非超管拒绝
  180. func TestCreateProduct_NonSuperAdminRejected(t *testing.T) {
  181. ctx := ctxhelper.AdminCtx("test_product")
  182. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  183. logic := NewCreateProductLogic(ctx, svcCtx)
  184. _, err := logic.CreateProduct(&types.CreateProductReq{Code: "test", Name: "test"})
  185. require.Error(t, err)
  186. var ce *response.CodeError
  187. require.True(t, errors.As(err, &ce))
  188. assert.Equal(t, 403, ce.Code())
  189. }
  190. // TC-0069~0593: createProduct 编码格式校验(M-8 修复验证)
  191. func TestCreateProduct_InvalidCodeFormat(t *testing.T) {
  192. ctx := ctxhelper.SuperAdminCtx()
  193. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  194. logic := NewCreateProductLogic(ctx, svcCtx)
  195. cases := []struct {
  196. name string
  197. code string
  198. }{
  199. {"空", ""},
  200. {"数字开头", "1abc"},
  201. {"下划线开头", "_abc"},
  202. {"中划线开头", "-abc"},
  203. {"包含中文", "产品A"},
  204. {"单字母(过短)", "a"},
  205. {"包含空格", "ab c"},
  206. {"包含特殊字符!", "ab!c"},
  207. {"包含斜杠", "ab/c"},
  208. }
  209. for _, c := range cases {
  210. t.Run(c.name, func(t *testing.T) {
  211. _, err := logic.CreateProduct(&types.CreateProductReq{Code: c.code, Name: "x"})
  212. require.Error(t, err, "code=%q 应被拒绝", c.code)
  213. var ce *response.CodeError
  214. require.True(t, errors.As(err, &ce))
  215. assert.Equal(t, 400, ce.Code())
  216. })
  217. }
  218. }
  219. // TC-0074: createProduct 编码长度>64 被拒绝
  220. func TestCreateProduct_CodeTooLong(t *testing.T) {
  221. ctx := ctxhelper.SuperAdminCtx()
  222. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  223. logic := NewCreateProductLogic(ctx, svcCtx)
  224. long := "a"
  225. for i := 0; i < 64; i++ {
  226. long += "b"
  227. }
  228. _, err := logic.CreateProduct(&types.CreateProductReq{Code: long, Name: "x"})
  229. require.Error(t, err)
  230. var ce *response.CodeError
  231. require.True(t, errors.As(err, &ce))
  232. assert.Equal(t, 400, ce.Code())
  233. }
  234. // TC-0075: createProduct 合法编码(包含下划线、中划线、数字)
  235. func TestCreateProduct_ValidCodeWithSymbols(t *testing.T) {
  236. ctx := ctxhelper.SuperAdminCtx()
  237. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  238. conn := testutil.GetTestSqlConn()
  239. code := "a_1-" + testutil.UniqueId()
  240. logic := NewCreateProductLogic(ctx, svcCtx)
  241. resp, err := logic.CreateProduct(&types.CreateProductReq{
  242. Code: code, Name: "x", AdminDeptId: seedAdminDept(t, ctx, svcCtx),
  243. })
  244. require.NoError(t, err)
  245. t.Cleanup(func() {
  246. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
  247. testutil.CleanTableByField(ctx, conn, "`sys_user`", "username", "admin_"+code)
  248. testutil.CleanTable(ctx, conn, "`sys_product`", resp.Id)
  249. })
  250. assert.Equal(t, code, resp.Code)
  251. }
  252. // suppress unused import
  253. var _ = (*productModel.SysProduct)(nil)