| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- package product
- import (
- "encoding/json"
- "errors"
- "sync"
- "testing"
- productModel "perms-system-server/internal/model/product"
- "perms-system-server/internal/response"
- "perms-system-server/internal/svc"
- "perms-system-server/internal/testutil"
- "perms-system-server/internal/testutil/ctxhelper"
- "perms-system-server/internal/types"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "golang.org/x/crypto/bcrypt"
- )
- // TC-0064: 正常创建
- func TestCreateProduct_Success(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- code := testutil.UniqueId()
- logic := NewCreateProductLogic(ctx, svcCtx)
- resp, err := logic.CreateProduct(&types.CreateProductReq{
- Code: code,
- Name: "测试产品",
- Remark: "集成测试",
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
- testutil.CleanTableByField(ctx, conn, "`sys_user`", "username", "admin_"+code)
- testutil.CleanTable(ctx, conn, "`sys_product`", resp.Id)
- })
- assert.True(t, resp.Id > 0)
- assert.Equal(t, code, resp.Code)
- assert.NotEmpty(t, resp.AppKey)
- assert.Equal(t, "admin_"+code, resp.AdminUser)
- // 审计 M-4:响应体必须不再明文携带 appSecret / adminPassword,
- // 改为发放一次性 credentialsTicket + 过期时间;调用方需凭 ticket 走
- // /api/product/fetchInitialCredentials 领取敏感凭证。
- assert.NotEmpty(t, resp.CredentialsTicket, "M-4:必须返回一次性凭证票据")
- assert.True(t, resp.CredentialsExpiresAt > 0, "M-4:必须返回过期时间戳")
- // 契约性校验:CreateProductResp 的 JSON 序列化里不应再出现 appSecret / adminPassword 字段。
- buf, err := json.Marshal(resp)
- require.NoError(t, err)
- var asMap map[string]interface{}
- require.NoError(t, json.Unmarshal(buf, &asMap))
- _, hasSecret := asMap["appSecret"]
- _, hasPwd := asMap["adminPassword"]
- assert.False(t, hasSecret, "M-4:CreateProductResp JSON 不得包含 appSecret 字段(避免日志落盘)")
- assert.False(t, hasPwd, "M-4:CreateProductResp JSON 不得包含 adminPassword 字段(避免日志落盘)")
- }
- // TC-0064: 正常创建
- func TestCreateProduct_VerifyDB(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- code := testutil.UniqueId()
- logic := NewCreateProductLogic(ctx, svcCtx)
- resp, err := logic.CreateProduct(&types.CreateProductReq{
- Code: code,
- Name: "DB验证产品",
- Remark: "验证数据库记录",
- })
- require.NoError(t, err)
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
- testutil.CleanTableByField(ctx, conn, "`sys_user`", "username", "admin_"+code)
- testutil.CleanTable(ctx, conn, "`sys_product`", resp.Id)
- })
- product, err := svcCtx.SysProductModel.FindOne(ctx, resp.Id)
- require.NoError(t, err)
- assert.Equal(t, code, product.Code)
- assert.Equal(t, "DB验证产品", product.Name)
- assert.Equal(t, resp.AppKey, product.AppKey)
- // 审计 M-4:CreateProduct 响应不再明文吐 appSecret;appSecret 经 ticket 领取后再核对。
- // 这里改为用 FetchInitialCredentialsLogic 把明文 appSecret 取出来,与 DB 中的 bcrypt hash 比对,
- // 既验证"DB 存的是 hash 而不是明文",也验证 ticket 流程正确交还了原始 appSecret。
- fetch := NewFetchInitialCredentialsLogic(ctx, svcCtx)
- cred, err := fetch.FetchInitialCredentials(&types.FetchInitialCredentialsReq{Ticket: resp.CredentialsTicket})
- require.NoError(t, err, "M-4:使用 ticket 必须能领取到初始 appSecret / adminPassword")
- require.NotEmpty(t, cred.AppSecret)
- require.NotEmpty(t, cred.AdminPassword)
- assert.NoError(t, bcrypt.CompareHashAndPassword([]byte(product.AppSecret), []byte(cred.AppSecret)),
- "DB should store bcrypt hash of appSecret, verifiable with plaintext from ticket payload")
- assert.Equal(t, int64(1), product.Status)
- var userCount int64
- err = conn.QueryRowCtx(ctx, &userCount,
- "SELECT COUNT(*) FROM `sys_user` WHERE `username` = ?", "admin_"+code)
- require.NoError(t, err)
- assert.Equal(t, int64(1), userCount)
- var memberCount int64
- err = conn.QueryRowCtx(ctx, &memberCount,
- "SELECT COUNT(*) FROM `sys_product_member` WHERE `productCode` = ? AND `memberType` = 'ADMIN'", code)
- require.NoError(t, err)
- assert.Equal(t, int64(1), memberCount)
- }
- // TC-0067: 编码已存在
- func TestCreateProduct_DuplicateCode(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- code := testutil.UniqueId()
- logic := NewCreateProductLogic(ctx, svcCtx)
- resp, err := logic.CreateProduct(&types.CreateProductReq{
- Code: code,
- Name: "第一个产品",
- })
- require.NoError(t, err)
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
- testutil.CleanTableByField(ctx, conn, "`sys_user`", "username", "admin_"+code)
- testutil.CleanTable(ctx, conn, "`sys_product`", resp.Id)
- })
- logic2 := NewCreateProductLogic(ctx, svcCtx)
- _, err = logic2.CreateProduct(&types.CreateProductReq{
- Code: code,
- Name: "重复产品",
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 409, codeErr.Code())
- assert.Equal(t, "产品编码已存在", codeErr.Error())
- }
- // TC-0068: 并发创建同编码
- func TestCreateProduct_ConcurrentSameCode(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- code := testutil.UniqueId()
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
- testutil.CleanTableByField(ctx, conn, "`sys_user`", "username", "admin_"+code)
- testutil.CleanTableByField(ctx, conn, "`sys_product`", "code", code)
- })
- var wg sync.WaitGroup
- results := make(chan error, 2)
- for i := 0; i < 2; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- logic := NewCreateProductLogic(ctx, svcCtx)
- _, err := logic.CreateProduct(&types.CreateProductReq{
- Code: code,
- Name: "并发测试产品",
- })
- results <- err
- }()
- }
- wg.Wait()
- close(results)
- var errs []error
- for err := range results {
- errs = append(errs, err)
- }
- require.Len(t, errs, 2)
- successCount := 0
- failCount := 0
- for _, err := range errs {
- if err == nil {
- successCount++
- } else {
- failCount++
- }
- }
- assert.Equal(t, 1, successCount, "exactly one goroutine should succeed")
- assert.Equal(t, 1, failCount, "exactly one goroutine should fail (409 or DB duplicate)")
- }
- // TC-0535: createProduct非超管拒绝
- func TestCreateProduct_NonSuperAdminRejected(t *testing.T) {
- ctx := ctxhelper.AdminCtx("test_product")
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateProductLogic(ctx, svcCtx)
- _, err := logic.CreateProduct(&types.CreateProductReq{Code: "test", Name: "test"})
- require.Error(t, err)
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 403, ce.Code())
- }
- // TC-0069~0593: createProduct 编码格式校验(M-8 修复验证)
- func TestCreateProduct_InvalidCodeFormat(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateProductLogic(ctx, svcCtx)
- cases := []struct {
- name string
- code string
- }{
- {"空", ""},
- {"数字开头", "1abc"},
- {"下划线开头", "_abc"},
- {"中划线开头", "-abc"},
- {"包含中文", "产品A"},
- {"单字母(过短)", "a"},
- {"包含空格", "ab c"},
- {"包含特殊字符!", "ab!c"},
- {"包含斜杠", "ab/c"},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- _, err := logic.CreateProduct(&types.CreateProductReq{Code: c.code, Name: "x"})
- require.Error(t, err, "code=%q 应被拒绝", c.code)
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 400, ce.Code())
- })
- }
- }
- // TC-0074: createProduct 编码长度>64 被拒绝
- func TestCreateProduct_CodeTooLong(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateProductLogic(ctx, svcCtx)
- long := "a"
- for i := 0; i < 64; i++ {
- long += "b"
- }
- _, err := logic.CreateProduct(&types.CreateProductReq{Code: long, Name: "x"})
- require.Error(t, err)
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 400, ce.Code())
- }
- // TC-0075: createProduct 合法编码(包含下划线、中划线、数字)
- func TestCreateProduct_ValidCodeWithSymbols(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- code := "a_1-" + testutil.UniqueId()
- logic := NewCreateProductLogic(ctx, svcCtx)
- resp, err := logic.CreateProduct(&types.CreateProductReq{Code: code, Name: "x"})
- require.NoError(t, err)
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
- testutil.CleanTableByField(ctx, conn, "`sys_user`", "username", "admin_"+code)
- testutil.CleanTable(ctx, conn, "`sys_product`", resp.Id)
- })
- assert.Equal(t, code, resp.Code)
- }
- // suppress unused import
- var _ = (*productModel.SysProduct)(nil)
|