package product import ( "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.NotEmpty(t, resp.AppSecret) assert.Equal(t, "admin_"+code, resp.AdminUser) assert.NotEmpty(t, resp.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) require.NoError(t, bcrypt.CompareHashAndPassword([]byte(product.AppSecret), []byte(resp.AppSecret)), "DB should store bcrypt hash of appSecret, verifiable with plaintext from response") 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)