createProductLogic_test.go 9.2 KB

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