|
@@ -0,0 +1,371 @@
|
|
|
|
|
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
|
|
+import { testClient } from 'hono/testing';
|
|
|
|
|
+import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
|
|
|
|
|
+import { JWTUtil } from '@d8d/shared-utils';
|
|
|
|
|
+import { z } from '@hono/zod-openapi';
|
|
|
|
|
+import { createCrudRoutes } from '../../src/routes/generic-crud.routes';
|
|
|
|
|
+import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
|
|
|
|
+
|
|
|
|
|
+// 测试实体类 - 包含decimal和bigint字段
|
|
|
|
|
+@Entity()
|
|
|
|
|
+class TestNumberEntity {
|
|
|
|
|
+ @PrimaryGeneratedColumn()
|
|
|
|
|
+ id!: number;
|
|
|
|
|
+
|
|
|
|
|
+ @Column('varchar')
|
|
|
|
|
+ name!: string;
|
|
|
|
|
+
|
|
|
|
|
+ @Column({ type: 'decimal', precision: 10, scale: 2, default: 0.00 })
|
|
|
|
|
+ price!: number;
|
|
|
|
|
+
|
|
|
|
|
+ @Column({ type: 'bigint', unsigned: true, default: 0 })
|
|
|
|
|
+ quantity!: number;
|
|
|
|
|
+
|
|
|
|
|
+ @Column({ type: 'decimal', precision: 15, scale: 4, nullable: true })
|
|
|
|
|
+ discount?: number;
|
|
|
|
|
+
|
|
|
|
|
+ @Column('int')
|
|
|
|
|
+ userId!: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 定义测试实体的Schema
|
|
|
|
|
+const createTestSchema = z.object({
|
|
|
|
|
+ name: z.string().min(1, '名称不能为空'),
|
|
|
|
|
+ price: z.coerce.number().positive('价格必须为正数'),
|
|
|
|
|
+ quantity: z.coerce.number().int().min(0, '数量必须为非负整数'),
|
|
|
|
|
+ discount: z.coerce.number().min(0).max(1).optional(),
|
|
|
|
|
+ userId: z.coerce.number().optional()
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const getTestSchema = z.object({
|
|
|
|
|
+ id: z.coerce.number(),
|
|
|
|
|
+ name: z.string(),
|
|
|
|
|
+ price: z.coerce.number(),
|
|
|
|
|
+ quantity: z.coerce.number(),
|
|
|
|
|
+ discount: z.coerce.number().nullable().optional(),
|
|
|
|
|
+ userId: z.coerce.number()
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const listTestSchema = z.object({
|
|
|
|
|
+ id: z.coerce.number(),
|
|
|
|
|
+ name: z.string(),
|
|
|
|
|
+ price: z.coerce.number(),
|
|
|
|
|
+ quantity: z.coerce.number(),
|
|
|
|
|
+ discount: z.coerce.number().nullable().optional(),
|
|
|
|
|
+ userId: z.coerce.number()
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const updateTestSchema = z.object({
|
|
|
|
|
+ name: z.string().min(1, '名称不能为空').optional(),
|
|
|
|
|
+ price: z.coerce.number().positive('价格必须为正数').optional(),
|
|
|
|
|
+ quantity: z.coerce.number().int().min(0, '数量必须为非负整数').optional(),
|
|
|
|
|
+ discount: z.coerce.number().min(0).max(1).optional()
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 设置集成测试钩子
|
|
|
|
|
+setupIntegrationDatabaseHooksWithEntities([TestNumberEntity])
|
|
|
|
|
+
|
|
|
|
|
+describe('共享CRUD Schema类型转换集成测试', () => {
|
|
|
|
|
+
|
|
|
|
|
+ let client: any;
|
|
|
|
|
+ let testToken: string;
|
|
|
|
|
+ const testUser = { id: 1, username: 'testuser' };
|
|
|
|
|
+
|
|
|
|
|
+ beforeEach(async () => {
|
|
|
|
|
+ // 创建测试路由
|
|
|
|
|
+ const app = createCrudRoutes({
|
|
|
|
|
+ entity: TestNumberEntity,
|
|
|
|
|
+ createSchema: createTestSchema,
|
|
|
|
|
+ updateSchema: updateTestSchema,
|
|
|
|
|
+ getSchema: getTestSchema,
|
|
|
|
|
+ listSchema: listTestSchema,
|
|
|
|
|
+ dataPermission: {
|
|
|
|
|
+ enabled: true,
|
|
|
|
|
+ userIdField: 'userId'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ client = testClient(app);
|
|
|
|
|
+ testToken = JWTUtil.generateToken(testUser);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // setupIntegrationDatabaseHooksWithEntities 已经自动处理了数据库清理
|
|
|
|
|
+
|
|
|
|
|
+ describe('创建操作 - 数字类型转换', () => {
|
|
|
|
|
+ it('应该正确处理decimal和bigint字段的创建和返回', async () => {
|
|
|
|
|
+ // 使用 z.coerce.number() 后,schema验证应该成功
|
|
|
|
|
+ const createData = {
|
|
|
|
|
+ name: '测试商品',
|
|
|
|
|
+ price: 99.99,
|
|
|
|
|
+ quantity: 1000,
|
|
|
|
|
+ discount: 0.1,
|
|
|
|
|
+ userId: testUser.id
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const response = await client['/'].$post({
|
|
|
|
|
+ json: createData
|
|
|
|
|
+ }, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ console.debug('创建商品响应状态:', response.status);
|
|
|
|
|
+ expect(response.status).toBe(201);
|
|
|
|
|
+
|
|
|
|
|
+ const result = await response.json();
|
|
|
|
|
+ console.debug('创建商品返回结果:', result);
|
|
|
|
|
+
|
|
|
|
|
+ // 验证返回的数据类型
|
|
|
|
|
+ expect(typeof result.price).toBe('number');
|
|
|
|
|
+ expect(result.price).toBe(99.99);
|
|
|
|
|
+ expect(typeof result.quantity).toBe('number');
|
|
|
|
|
+ expect(result.quantity).toBe(1000);
|
|
|
|
|
+ expect(typeof result.discount).toBe('number');
|
|
|
|
|
+ expect(result.discount).toBe(0.1);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该处理没有discount字段的创建', async () => {
|
|
|
|
|
+ const createData = {
|
|
|
|
|
+ name: '测试商品无折扣',
|
|
|
|
|
+ price: 50.00,
|
|
|
|
|
+ quantity: 500,
|
|
|
|
|
+ userId: testUser.id
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const response = await client['/'].$post({
|
|
|
|
|
+ json: createData
|
|
|
|
|
+ }, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ expect(response.status).toBe(201);
|
|
|
|
|
+
|
|
|
|
|
+ const result = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ // 验证返回的数据类型
|
|
|
|
|
+ expect(typeof result.price).toBe('number');
|
|
|
|
|
+ expect(result.price).toBe(50.00);
|
|
|
|
|
+ expect(typeof result.quantity).toBe('number');
|
|
|
|
|
+ expect(result.quantity).toBe(500);
|
|
|
|
|
+ expect(result.discount).toBeNull(); // 没有discount字段应该返回null
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('更新操作 - 数字类型转换', () => {
|
|
|
|
|
+ let createdId: number;
|
|
|
|
|
+
|
|
|
|
|
+ beforeEach(async () => {
|
|
|
|
|
+ // 先创建一个测试数据
|
|
|
|
|
+ const createData = {
|
|
|
|
|
+ name: '原始商品',
|
|
|
|
|
+ price: 100.00,
|
|
|
|
|
+ quantity: 100,
|
|
|
|
|
+ discount: 0.2,
|
|
|
|
|
+ userId: testUser.id
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const response = await client['/'].$post({
|
|
|
|
|
+ json: createData
|
|
|
|
|
+ }, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const result = await response.json();
|
|
|
|
|
+ createdId = result.id;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该正确处理decimal和bigint字段的更新和返回', async () => {
|
|
|
|
|
+ const updateData = {
|
|
|
|
|
+ name: '更新后的商品',
|
|
|
|
|
+ price: 88.88,
|
|
|
|
|
+ quantity: 888,
|
|
|
|
|
+ discount: 0.15
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const response = await client['/:id'].$put({
|
|
|
|
|
+ param: { id: createdId },
|
|
|
|
|
+ json: updateData
|
|
|
|
|
+ }, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ console.debug('更新商品响应状态:', response.status);
|
|
|
|
|
+ expect(response.status).toBe(200);
|
|
|
|
|
+
|
|
|
|
|
+ const result = await response.json();
|
|
|
|
|
+ console.debug('更新商品返回结果:', result);
|
|
|
|
|
+
|
|
|
|
|
+ // 验证返回的数据类型
|
|
|
|
|
+ expect(result.id).toBe(createdId);
|
|
|
|
|
+ expect(result.name).toBe(updateData.name);
|
|
|
|
|
+
|
|
|
|
|
+ // 关键验证:更新后的price字段应该是数字类型
|
|
|
|
|
+ expect(typeof result.price).toBe('number');
|
|
|
|
|
+ expect(result.price).toBe(88.88);
|
|
|
|
|
+
|
|
|
|
|
+ // 关键验证:更新后的quantity字段应该是数字类型
|
|
|
|
|
+ expect(typeof result.quantity).toBe('number');
|
|
|
|
|
+ expect(result.quantity).toBe(888);
|
|
|
|
|
+
|
|
|
|
|
+ // 关键验证:更新后的discount字段应该是数字类型
|
|
|
|
|
+ expect(typeof result.discount).toBe('number');
|
|
|
|
|
+ expect(result.discount).toBe(0.15);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该处理部分字段的更新', async () => {
|
|
|
|
|
+ const updateData = {
|
|
|
|
|
+ price: 66.66
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const response = await client['/:id'].$put({
|
|
|
|
|
+ param: { id: createdId },
|
|
|
|
|
+ json: updateData
|
|
|
|
|
+ }, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ expect(response.status).toBe(200);
|
|
|
|
|
+
|
|
|
|
|
+ const result = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ // 验证只有price字段被更新,其他字段保持不变
|
|
|
|
|
+ expect(result.name).toBe('原始商品');
|
|
|
|
|
+ expect(typeof result.price).toBe('number');
|
|
|
|
|
+ expect(result.price).toBe(66.66);
|
|
|
|
|
+ expect(typeof result.quantity).toBe('number');
|
|
|
|
|
+ expect(result.quantity).toBe(100); // 原始值
|
|
|
|
|
+ expect(typeof result.discount).toBe('number');
|
|
|
|
|
+ expect(result.discount).toBe(0.2); // 原始值
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('获取操作 - 数字类型转换', () => {
|
|
|
|
|
+ let createdId: number;
|
|
|
|
|
+
|
|
|
|
|
+ beforeEach(async () => {
|
|
|
|
|
+ // 先创建一个测试数据
|
|
|
|
|
+ const createData = {
|
|
|
|
|
+ name: '查询测试商品',
|
|
|
|
|
+ price: 123.45,
|
|
|
|
|
+ quantity: 999,
|
|
|
|
|
+ discount: 0.05,
|
|
|
|
|
+ userId: testUser.id
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const response = await client['/'].$post({
|
|
|
|
|
+ json: createData
|
|
|
|
|
+ }, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const result = await response.json();
|
|
|
|
|
+ createdId = result.id;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该正确处理decimal和bigint字段的查询返回', async () => {
|
|
|
|
|
+ const response = await client['/:id'].$get({
|
|
|
|
|
+ param: { id: createdId }
|
|
|
|
|
+ }, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ console.debug('获取商品响应状态:', response.status);
|
|
|
|
|
+ expect(response.status).toBe(200);
|
|
|
|
|
+
|
|
|
|
|
+ const result = await response.json();
|
|
|
|
|
+ console.debug('获取商品返回结果:', result);
|
|
|
|
|
+
|
|
|
|
|
+ // 验证返回的数据类型
|
|
|
|
|
+ expect(result.id).toBe(createdId);
|
|
|
|
|
+ expect(result.name).toBe('查询测试商品');
|
|
|
|
|
+
|
|
|
|
|
+ // 关键验证:price字段应该是数字类型
|
|
|
|
|
+ expect(typeof result.price).toBe('number');
|
|
|
|
|
+ expect(result.price).toBe(123.45);
|
|
|
|
|
+
|
|
|
|
|
+ // 关键验证:quantity字段应该是数字类型
|
|
|
|
|
+ expect(typeof result.quantity).toBe('number');
|
|
|
|
|
+ expect(result.quantity).toBe(999);
|
|
|
|
|
+
|
|
|
|
|
+ // 关键验证:discount字段应该是数字类型
|
|
|
|
|
+ expect(typeof result.discount).toBe('number');
|
|
|
|
|
+ expect(result.discount).toBe(0.05);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('列表查询 - 数字类型转换', () => {
|
|
|
|
|
+ beforeEach(async () => {
|
|
|
|
|
+ // 创建多个测试数据
|
|
|
|
|
+ const testData = [
|
|
|
|
|
+ { name: '商品1', price: 10.50, quantity: 100, discount: 0.1, userId: testUser.id },
|
|
|
|
|
+ { name: '商品2', price: 20.75, quantity: 200, discount: 0.2, userId: testUser.id },
|
|
|
|
|
+ { name: '商品3', price: 30.99, quantity: 300, userId: testUser.id } // 没有discount
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ for (const data of testData) {
|
|
|
|
|
+ await client['/'].$post({
|
|
|
|
|
+ json: data
|
|
|
|
|
+ }, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该正确处理列表查询中的数字类型转换', async () => {
|
|
|
|
|
+ const response = await client['/'].$get({
|
|
|
|
|
+ query: { page: 1, pageSize: 10 }
|
|
|
|
|
+ }, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ console.debug('列表查询响应状态:', response.status);
|
|
|
|
|
+ expect(response.status).toBe(200);
|
|
|
|
|
+
|
|
|
|
|
+ const result = await response.json();
|
|
|
|
|
+ console.debug('列表查询返回结果:', result);
|
|
|
|
|
+
|
|
|
|
|
+ expect(result).toHaveProperty('data');
|
|
|
|
|
+ expect(result).toHaveProperty('pagination');
|
|
|
|
|
+ expect(Array.isArray(result.data)).toBe(true);
|
|
|
|
|
+
|
|
|
|
|
+ // 验证每个商品的数字字段类型
|
|
|
|
|
+ result.data.forEach((item: any) => {
|
|
|
|
|
+ expect(typeof item.price).toBe('number');
|
|
|
|
|
+ expect(typeof item.quantity).toBe('number');
|
|
|
|
|
+
|
|
|
|
|
+ // discount字段可能为数字或null
|
|
|
|
|
+ if (item.discount !== null) {
|
|
|
|
|
+ expect(typeof item.discount).toBe('number');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 验证具体数值
|
|
|
|
|
+ const item1 = result.data.find((item: any) => item.name === '商品1');
|
|
|
|
|
+ expect(item1).toBeDefined();
|
|
|
|
|
+ expect(item1.price).toBe(10.50);
|
|
|
|
|
+ expect(item1.quantity).toBe(100);
|
|
|
|
|
+ expect(item1.discount).toBe(0.1);
|
|
|
|
|
+
|
|
|
|
|
+ const item3 = result.data.find((item: any) => item.name === '商品3');
|
|
|
|
|
+ expect(item3).toBeDefined();
|
|
|
|
|
+ expect(item3.discount).toBeNull();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+});
|