备份:修复前完整项目快照 2026-04-19
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
const db = require('../db-sqlite');
|
||||
|
||||
describe('预支核销状态管理测试', () => {
|
||||
let advanceId;
|
||||
let verificationId;
|
||||
|
||||
beforeAll(async () => {
|
||||
// 清空测试数据
|
||||
await db.query('DELETE FROM verifications WHERE 1=1');
|
||||
await db.query('DELETE FROM advances WHERE 1=1');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
await db.query('DELETE FROM verifications WHERE 1=1');
|
||||
await db.query('DELETE FROM advances WHERE 1=1');
|
||||
});
|
||||
|
||||
// 测试1: 创建预支单
|
||||
test('创建预支单', async () => {
|
||||
const result = await db.query(
|
||||
'INSERT INTO advances (user_id, project_id, amount, currency, reason, advance_date, advance_code, status, applicant) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[1, 1, 1000, 'CNY', '测试预支', '2026-03-25', 'ADV-TEST-001', 'pending', '隆林']
|
||||
);
|
||||
advanceId = result.lastID;
|
||||
expect(advanceId).toBeDefined();
|
||||
});
|
||||
|
||||
// 测试2: 审批预支单
|
||||
test('审批预支单', async () => {
|
||||
await db.query('UPDATE advances SET status = ? WHERE id = ?', ['approved', advanceId]);
|
||||
const advance = await db.query('SELECT * FROM advances WHERE id = ?', [advanceId]);
|
||||
expect(advance.rows[0].status).toBe('approved');
|
||||
});
|
||||
|
||||
// 测试3: 执行预支单
|
||||
test('执行预支单', async () => {
|
||||
await db.query('UPDATE advances SET status = ? WHERE id = ?', ['executed', advanceId]);
|
||||
const advance = await db.query('SELECT * FROM advances WHERE id = ?', [advanceId]);
|
||||
expect(advance.rows[0].status).toBe('executed');
|
||||
});
|
||||
|
||||
// 测试4: 创建部分核销单
|
||||
test('创建部分核销单', async () => {
|
||||
const result = await db.query(
|
||||
'INSERT INTO verifications (advance_id, amount, currency, reason, verification_date, verification_code, status, applicant, advance_code, advance_amount, settlement) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[advanceId, 300, 'CNY', '部分核销', '2026-03-25', 'VER-TEST-001', 'pending', '隆林', 'ADV-TEST-001', 1000, 0]
|
||||
);
|
||||
verificationId = result.lastID;
|
||||
expect(verificationId).toBeDefined();
|
||||
});
|
||||
|
||||
// 测试5: 审批核销单
|
||||
test('审批核销单', async () => {
|
||||
await db.query('UPDATE verifications SET status = ? WHERE id = ?', ['approved', verificationId]);
|
||||
const verification = await db.query('SELECT * FROM verifications WHERE id = ?', [verificationId]);
|
||||
expect(verification.rows[0].status).toBe('approved');
|
||||
});
|
||||
|
||||
// 测试6: 执行核销单
|
||||
test('执行核销单', async () => {
|
||||
await db.query('UPDATE verifications SET status = ? WHERE id = ?', ['executed', verificationId]);
|
||||
// 更新预支单状态为部分核销
|
||||
await db.query('UPDATE advances SET status = ? WHERE id = ?', ['partial_verification', advanceId]);
|
||||
// 更新预支单已核销金额
|
||||
await db.query('UPDATE advances SET total_reimbursed = ? WHERE id = ?', [300, advanceId]);
|
||||
|
||||
const verification = await db.query('SELECT * FROM verifications WHERE id = ?', [verificationId]);
|
||||
expect(verification.rows[0].status).toBe('executed');
|
||||
});
|
||||
|
||||
// 测试7: 检查预支单状态
|
||||
test('检查预支单状态', async () => {
|
||||
const advance = await db.query('SELECT * FROM advances WHERE id = ?', [advanceId]);
|
||||
expect(advance.rows[0].total_reimbursed).toBe(300);
|
||||
expect(advance.rows[0].status).toBe('partial_verification');
|
||||
});
|
||||
|
||||
// 测试8: 创建结算核销单(退款)
|
||||
test('创建结算核销单(退款)', async () => {
|
||||
await db.query(
|
||||
'INSERT INTO verifications (advance_id, amount, currency, reason, verification_date, verification_code, status, applicant, advance_code, advance_amount, settlement, settlement_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[advanceId, 600, 'CNY', '结算核销(退款)', '2026-03-25', 'VER-TEST-002', 'executed', '隆林', 'ADV-TEST-001', 1000, 1, 100]
|
||||
);
|
||||
// 更新预支单状态为已完成
|
||||
await db.query('UPDATE advances SET status = ? WHERE id = ?', ['completed', advanceId]);
|
||||
// 更新预支单已核销金额
|
||||
await db.query('UPDATE advances SET total_reimbursed = ? WHERE id = ?', [900, advanceId]);
|
||||
});
|
||||
|
||||
// 测试9: 检查预支单是否已完结
|
||||
test('检查预支单是否已完结', async () => {
|
||||
const advance = await db.query('SELECT * FROM advances WHERE id = ?', [advanceId]);
|
||||
expect(advance.rows[0].total_reimbursed).toBe(900);
|
||||
expect(advance.rows[0].status).toBe('completed');
|
||||
});
|
||||
|
||||
// 测试10: 获取预支核销状态列表
|
||||
test('获取预支核销状态列表', async () => {
|
||||
const result = await db.query('SELECT * FROM advances');
|
||||
expect(Array.isArray(result.rows)).toBe(true);
|
||||
expect(result.rows.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* 分类管理API测试
|
||||
* TDD: 先写测试,再实现功能
|
||||
*/
|
||||
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
|
||||
// 模拟Express应用
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// 测试数据
|
||||
const testCategories = [
|
||||
{ name: '测试一级分类1', parent_id: null, level: 1 },
|
||||
{ name: '测试一级分类2', parent_id: null, level: 1 },
|
||||
];
|
||||
|
||||
describe('分类管理API测试', () => {
|
||||
|
||||
// 测试1: 获取分类树
|
||||
describe('GET /api/categories/tree', () => {
|
||||
test('应该返回分类树结构', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/categories/tree')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
// 每个一级分类应该有children属性
|
||||
if (response.body.data.length > 0) {
|
||||
expect(response.body.data[0]).toHaveProperty('children');
|
||||
}
|
||||
});
|
||||
|
||||
test('应该支持按层级筛选', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/categories/tree?level=1')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
// 返回的都是一级分类
|
||||
response.body.data.forEach(cat => {
|
||||
expect(cat.level).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 测试2: 创建分类
|
||||
describe('POST /api/categories', () => {
|
||||
test('应该能创建一级分类', async () => {
|
||||
const newCategory = {
|
||||
name: '新材料分类',
|
||||
parent_id: null,
|
||||
level: 1,
|
||||
description: '测试描述'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/categories')
|
||||
.send(newCategory)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toHaveProperty('id');
|
||||
expect(response.body.data.name).toBe(newCategory.name);
|
||||
});
|
||||
|
||||
test('应该能在指定父分类下创建二级分类', async () => {
|
||||
// 先创建父分类
|
||||
const parentResponse = await request(app)
|
||||
.post('/api/categories')
|
||||
.send({ name: '父分类', parent_id: null, level: 1 });
|
||||
|
||||
const parentId = parentResponse.body.data.id;
|
||||
|
||||
// 创建子分类
|
||||
const childCategory = {
|
||||
name: '子分类',
|
||||
parent_id: parentId,
|
||||
level: 2
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/categories')
|
||||
.send(childCategory)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data.parent_id).toBe(parentId);
|
||||
expect(response.body.data.level).toBe(2);
|
||||
});
|
||||
|
||||
test('分类名称不能为空', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/categories')
|
||||
.send({ name: '', parent_id: null, level: 1 })
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toContain('名称');
|
||||
});
|
||||
|
||||
test('同一父分类下不能有重名子分类', async () => {
|
||||
// 创建父分类
|
||||
const parent = await request(app)
|
||||
.post('/api/categories')
|
||||
.send({ name: '唯一父分类', parent_id: null, level: 1 });
|
||||
|
||||
// 创建第一个子分类
|
||||
await request(app)
|
||||
.post('/api/categories')
|
||||
.send({ name: '同名子分类', parent_id: parent.body.data.id, level: 2 });
|
||||
|
||||
// 尝试创建同名子分类
|
||||
const response = await request(app)
|
||||
.post('/api/categories')
|
||||
.send({ name: '同名子分类', parent_id: parent.body.data.id, level: 2 })
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// 测试3: 更新分类
|
||||
describe('PUT /api/categories/:id', () => {
|
||||
test('应该能更新分类名称', async () => {
|
||||
// 先创建分类
|
||||
const createResponse = await request(app)
|
||||
.post('/api/categories')
|
||||
.send({ name: '原名称', parent_id: null, level: 1 });
|
||||
|
||||
const categoryId = createResponse.body.data.id;
|
||||
|
||||
// 更新分类
|
||||
const response = await request(app)
|
||||
.put(`/api/categories/${categoryId}`)
|
||||
.send({ name: '新名称' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
|
||||
// 验证更新
|
||||
const getResponse = await request(app)
|
||||
.get(`/api/categories/${categoryId}`)
|
||||
.expect(200);
|
||||
|
||||
expect(getResponse.body.data.name).toBe('新名称');
|
||||
});
|
||||
|
||||
test('不能将分类设置为自己的子分类', async () => {
|
||||
// 创建父分类
|
||||
const parent = await request(app)
|
||||
.post('/api/categories')
|
||||
.send({ name: '父', parent_id: null, level: 1 });
|
||||
|
||||
// 创建子分类
|
||||
const child = await request(app)
|
||||
.post('/api/categories')
|
||||
.send({ name: '子', parent_id: parent.body.data.id, level: 2 });
|
||||
|
||||
// 尝试将父分类的parent_id设为子分类(循环引用)
|
||||
const response = await request(app)
|
||||
.put(`/api/categories/${parent.body.data.id}`)
|
||||
.send({ parent_id: child.body.data.id })
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// 测试4: 删除分类
|
||||
describe('DELETE /api/categories/:id', () => {
|
||||
test('应该能删除空分类', async () => {
|
||||
// 创建分类
|
||||
const createResponse = await request(app)
|
||||
.post('/api/categories')
|
||||
.send({ name: '待删除', parent_id: null, level: 1 });
|
||||
|
||||
const categoryId = createResponse.body.data.id;
|
||||
|
||||
// 删除分类
|
||||
const response = await request(app)
|
||||
.delete(`/api/categories/${categoryId}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
|
||||
// 验证已删除
|
||||
const getResponse = await request(app)
|
||||
.get(`/api/categories/${categoryId}`)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
test('删除一级分类应该级联删除二级分类', async () => {
|
||||
// 创建父分类
|
||||
const parent = await request(app)
|
||||
.post('/api/categories')
|
||||
.send({ name: '父', parent_id: null, level: 1 });
|
||||
|
||||
// 创建子分类
|
||||
const child = await request(app)
|
||||
.post('/api/categories')
|
||||
.send({ name: '子', parent_id: parent.body.data.id, level: 2 });
|
||||
|
||||
// 删除父分类
|
||||
await request(app)
|
||||
.delete(`/api/categories/${parent.body.data.id}`)
|
||||
.expect(200);
|
||||
|
||||
// 验证子分类也被删除
|
||||
await request(app)
|
||||
.get(`/api/categories/${child.body.data.id}`)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
test('有商品的分类不应该被删除', async () => {
|
||||
// 创建分类
|
||||
const category = await request(app)
|
||||
.post('/api/categories')
|
||||
.send({ name: '有商品的分类', parent_id: null, level: 1 });
|
||||
|
||||
// 在该分类下创建商品(模拟)
|
||||
// ... 创建商品逻辑
|
||||
|
||||
// 尝试删除分类
|
||||
const response = await request(app)
|
||||
.delete(`/api/categories/${category.body.data.id}`)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toContain('商品');
|
||||
});
|
||||
});
|
||||
|
||||
// 测试5: 批量导入时自动创建分类
|
||||
describe('批量导入分类处理', () => {
|
||||
test('导入时应该自动创建不存在的一级分类', async () => {
|
||||
const importData = {
|
||||
products: [
|
||||
{
|
||||
name: '测试商品',
|
||||
category_level1: '新一级分类',
|
||||
category_level2: '新二级分类'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/products/batch-import')
|
||||
.send(importData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
|
||||
// 验证分类已创建
|
||||
const categories = await request(app)
|
||||
.get('/api/categories/tree')
|
||||
.expect(200);
|
||||
|
||||
const level1Exists = categories.body.data.some(
|
||||
cat => cat.name === '新一级分类'
|
||||
);
|
||||
expect(level1Exists).toBe(true);
|
||||
});
|
||||
|
||||
test('导入时应该自动创建不存在的二级分类', async () => {
|
||||
// 先创建一级分类
|
||||
await request(app)
|
||||
.post('/api/categories')
|
||||
.send({ name: '已有的一级', parent_id: null, level: 1 });
|
||||
|
||||
// 导入带有新二级分类的商品
|
||||
const importData = {
|
||||
products: [
|
||||
{
|
||||
name: '测试商品2',
|
||||
category_level1: '已有的一级',
|
||||
category_level2: '新的二级'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await request(app)
|
||||
.post('/api/products/batch-import')
|
||||
.send(importData)
|
||||
.expect(200);
|
||||
|
||||
// 验证二级分类已创建
|
||||
const categories = await request(app)
|
||||
.get('/api/categories/tree')
|
||||
.expect(200);
|
||||
|
||||
const parent = categories.body.data.find(
|
||||
cat => cat.name === '已有的一级'
|
||||
);
|
||||
expect(parent).toBeTruthy();
|
||||
expect(parent.children).toBeDefined();
|
||||
|
||||
const level2Exists = parent.children.some(
|
||||
child => child.name === '新的二级'
|
||||
);
|
||||
expect(level2Exists).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 运行测试
|
||||
if (require.main === module) {
|
||||
const { execSync } = require('child_process');
|
||||
try {
|
||||
execSync('npx jest test-category-api.spec.js --verbose', {
|
||||
cwd: __dirname,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
} catch (e) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { app };
|
||||
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* 分类树结构测试
|
||||
* TDD: 先写测试,再实现功能
|
||||
*/
|
||||
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
// 使用测试数据库
|
||||
const TEST_DB_PATH = path.join(__dirname, '../test_company_finance.db');
|
||||
|
||||
// 测试数据库连接
|
||||
function createTestDb() {
|
||||
return new sqlite3.Database(TEST_DB_PATH);
|
||||
}
|
||||
|
||||
// 初始化测试数据库
|
||||
async function initTestDb(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 读取迁移脚本
|
||||
const fs = require('fs');
|
||||
const migrationScript = fs.readFileSync(
|
||||
path.join(__dirname, '../migrations/001_create_category_tree.sql'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
db.exec(migrationScript, (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 测试套件
|
||||
describe('分类树结构测试', () => {
|
||||
let db;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = createTestDb();
|
||||
await initTestDb(db);
|
||||
});
|
||||
|
||||
afterAll((done) => {
|
||||
db.close(() => {
|
||||
// 清理测试数据库
|
||||
const fs = require('fs');
|
||||
try {
|
||||
fs.unlinkSync(TEST_DB_PATH);
|
||||
} catch (e) {
|
||||
// 忽略删除错误
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// 测试1: 分类表应该存在
|
||||
test('category_tree 表应该存在', async () => {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='category_tree'",
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.name).toBe('category_tree');
|
||||
});
|
||||
|
||||
// 测试2: 应该能创建一级分类
|
||||
test('应该能创建一级分类', async () => {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO category_tree (name, parent_id, level, sort_order) VALUES (?, ?, ?, ?)',
|
||||
['测试一级分类', null, 1, 1],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.id).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// 测试3: 应该能在一级分类下创建二级分类
|
||||
test('应该能创建二级分类', async () => {
|
||||
// 先创建一级分类
|
||||
const parentResult = await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO category_tree (name, parent_id, level, sort_order) VALUES (?, ?, ?, ?)',
|
||||
['父分类', null, 1, 1],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// 再创建二级分类
|
||||
const childResult = await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO category_tree (name, parent_id, level, sort_order) VALUES (?, ?, ?, ?)',
|
||||
['子分类', parentResult.id, 2, 1],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
expect(childResult.id).toBeGreaterThan(0);
|
||||
|
||||
// 验证父子关系
|
||||
const childCategory = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
'SELECT * FROM category_tree WHERE id = ?',
|
||||
[childResult.id],
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
expect(childCategory.parent_id).toBe(parentResult.id);
|
||||
expect(childCategory.level).toBe(2);
|
||||
});
|
||||
|
||||
// 测试4: 应该能获取分类树
|
||||
test('应该能获取完整的分类树', async () => {
|
||||
const categories = await new Promise((resolve, reject) => {
|
||||
db.all(
|
||||
'SELECT * FROM category_tree ORDER BY level, sort_order',
|
||||
(err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
expect(Array.isArray(categories)).toBe(true);
|
||||
expect(categories.length).toBeGreaterThan(0);
|
||||
|
||||
// 验证有默认分类数据
|
||||
const defaultCategories = categories.filter(c =>
|
||||
['电杆横担', '电缆电线', '变压器'].includes(c.name)
|
||||
);
|
||||
expect(defaultCategories.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// 测试5: 删除一级分类应该级联删除二级分类
|
||||
test('删除一级分类应该级联删除二级分类', async () => {
|
||||
// 创建测试数据
|
||||
const parent = await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO category_tree (name, parent_id, level) VALUES (?, ?, ?)',
|
||||
['待删除父分类', null, 1],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const child = await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO category_tree (name, parent_id, level) VALUES (?, ?, ?)',
|
||||
['待删除子分类', parent.id, 2],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// 删除父分类
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run('DELETE FROM category_tree WHERE id = ?', [parent.id], function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// 验证子分类也被删除
|
||||
const remainingChild = await new Promise((resolve, reject) => {
|
||||
db.get('SELECT * FROM category_tree WHERE id = ?', [child.id], (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
expect(remainingChild).toBeUndefined();
|
||||
});
|
||||
|
||||
// 测试6: 应该能更新分类
|
||||
test('应该能更新分类', async () => {
|
||||
// 创建分类
|
||||
const category = await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO category_tree (name, parent_id, level) VALUES (?, ?, ?)',
|
||||
['原名称', null, 1],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// 更新名称
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'UPDATE category_tree SET name = ? WHERE id = ?',
|
||||
['新名称', category.id],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// 验证更新
|
||||
const updated = await new Promise((resolve, reject) => {
|
||||
db.get('SELECT * FROM category_tree WHERE id = ?', [category.id], (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
expect(updated.name).toBe('新名称');
|
||||
});
|
||||
});
|
||||
|
||||
// 商品表测试
|
||||
describe('商品表测试', () => {
|
||||
let db;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = createTestDb();
|
||||
await initTestDb(db);
|
||||
});
|
||||
|
||||
afterAll((done) => {
|
||||
db.close(done);
|
||||
});
|
||||
|
||||
// 测试1: 商品表应该存在
|
||||
test('products 表应该存在', async () => {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='products'",
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.name).toBe('products');
|
||||
});
|
||||
|
||||
// 测试2: 应该能创建商品
|
||||
test('应该能创建商品', async () => {
|
||||
// 先获取一个分类ID
|
||||
const category = await new Promise((resolve, reject) => {
|
||||
db.get('SELECT id FROM category_tree LIMIT 1', (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
`INSERT INTO products (
|
||||
name, model, category_id, category_name, unit,
|
||||
cost_price, price, brand, specification, source, remark
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
'测试商品', 'Model-001', category.id, '测试分类', '件',
|
||||
100.00, 150.00, '测试品牌', '规格参数', '中国', '测试备注'
|
||||
],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.id).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// 测试3: 商品应该有默认值
|
||||
test('商品字段应该有正确的默认值', async () => {
|
||||
const category = await new Promise((resolve, reject) => {
|
||||
db.get('SELECT id FROM category_tree LIMIT 1', (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO products (name, category_id) VALUES (?, ?)',
|
||||
['最小化商品', category.id],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const product = await new Promise((resolve, reject) => {
|
||||
db.get('SELECT * FROM products WHERE id = ?', [result.id], (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
expect(product.unit).toBe('件');
|
||||
expect(product.source).toBe('老挝');
|
||||
expect(product.status).toBe('active');
|
||||
expect(product.stock_quantity).toBe(0);
|
||||
});
|
||||
|
||||
// 测试4: cost_price 可以为空
|
||||
test('cost_price 应该可以为空', async () => {
|
||||
const category = await new Promise((resolve, reject) => {
|
||||
db.get('SELECT id FROM category_tree LIMIT 1', (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO products (name, category_id, cost_price) VALUES (?, ?, ?)',
|
||||
['无成本商品', category.id, null],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.id).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// 运行测试
|
||||
if (require.main === module) {
|
||||
const { execSync } = require('child_process');
|
||||
try {
|
||||
execSync('npx jest test-category-tree.spec.js --verbose', {
|
||||
cwd: __dirname,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
} catch (e) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createTestDb, initTestDb };
|
||||
@@ -0,0 +1,156 @@
|
||||
const request = require('supertest');
|
||||
const app = require('../final-backend');
|
||||
|
||||
describe('采购付款分离 - API层测试', () => {
|
||||
describe('采购申请API', () => {
|
||||
let testPurchaseRequestId;
|
||||
|
||||
test('POST /api/purchase-requests - 应该能创建采购申请', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/purchase-requests')
|
||||
.send({
|
||||
project_id: 1,
|
||||
applicant: '测试申请人',
|
||||
request_date: '2026-03-25',
|
||||
expense_category: 'material',
|
||||
total_amount: 15000,
|
||||
items: [
|
||||
{
|
||||
product_name: '测试商品A',
|
||||
quantity: 10,
|
||||
unit_price: 1000,
|
||||
total_price: 10000
|
||||
},
|
||||
{
|
||||
product_name: '测试商品B',
|
||||
quantity: 5,
|
||||
unit_price: 1000,
|
||||
total_price: 5000
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toHaveProperty('id');
|
||||
expect(response.body.data).toHaveProperty('request_code');
|
||||
testPurchaseRequestId = response.body.data.id;
|
||||
});
|
||||
|
||||
test('GET /api/purchase-requests - 应该能获取采购申请列表', async () => {
|
||||
const response = await request(app).get('/api/purchase-requests');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
});
|
||||
|
||||
test('GET /api/purchase-requests/:id - 应该能获取采购申请详情', async () => {
|
||||
const response = await request(app).get(`/api/purchase-requests/${testPurchaseRequestId}`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toHaveProperty('items');
|
||||
expect(response.body.data.items.length).toBe(2);
|
||||
});
|
||||
|
||||
test('POST /api/purchase-requests/:id/submit - 应该能提交采购申请', async () => {
|
||||
const response = await request(app)
|
||||
.post(`/api/purchase-requests/${testPurchaseRequestId}/submit`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
});
|
||||
|
||||
test('POST /api/purchase-requests/:id/approve - 应该能审批通过采购申请', async () => {
|
||||
const response = await request(app)
|
||||
.post(`/api/purchase-requests/${testPurchaseRequestId}/approve`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
});
|
||||
|
||||
test('PUT /api/purchase-requests/:id - 应该能更新采购申请', async () => {
|
||||
const response = await request(app)
|
||||
.put(`/api/purchase-requests/${testPurchaseRequestId}`)
|
||||
.send({
|
||||
project_id: 1,
|
||||
applicant: '测试申请人',
|
||||
request_date: '2026-03-25',
|
||||
expense_category: 'material',
|
||||
total_amount: 20000,
|
||||
remark: '更新备注'
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('库存管理API', () => {
|
||||
test('GET /api/inventory - 应该能获取库存记录', async () => {
|
||||
const response = await request(app).get('/api/inventory');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
});
|
||||
|
||||
test('GET /api/inventory/summary - 应该能获取库存汇总', async () => {
|
||||
const response = await request(app).get('/api/inventory/summary');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
});
|
||||
|
||||
test('POST /api/inventory/out - 应该能创建出库记录', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/inventory/out')
|
||||
.send({
|
||||
project_id: 1,
|
||||
product_id: 1,
|
||||
quantity: 10,
|
||||
operator: '测试操作员'
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('项目成本统计API', () => {
|
||||
test('GET /api/projects/:id/cost-summary - 应该能获取项目成本统计', async () => {
|
||||
const response = await request(app).get('/api/projects/1/cost-summary');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toHaveProperty('project_name');
|
||||
expect(response.body.data).toHaveProperty('purchase_cost');
|
||||
expect(response.body.data).toHaveProperty('payment_cost');
|
||||
expect(response.body.data).toHaveProperty('total_cost');
|
||||
expect(response.body.data).toHaveProperty('profit');
|
||||
});
|
||||
});
|
||||
|
||||
describe('采购申请删除API', () => {
|
||||
let tempPurchaseRequestId;
|
||||
|
||||
beforeAll(async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/purchase-requests')
|
||||
.send({
|
||||
project_id: 1,
|
||||
applicant: '临时测试',
|
||||
request_date: '2026-03-25',
|
||||
expense_category: 'material',
|
||||
total_amount: 1000
|
||||
});
|
||||
tempPurchaseRequestId = response.body.data.id;
|
||||
});
|
||||
|
||||
test('DELETE /api/purchase-requests/:id - 应该能删除采购申请', async () => {
|
||||
const response = await request(app)
|
||||
.delete(`/api/purchase-requests/${tempPurchaseRequestId}`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
});
|
||||
|
||||
test('GET /api/purchase-requests/:id - 删除后应该返回404', async () => {
|
||||
const response = await request(app)
|
||||
.get(`/api/purchase-requests/${tempPurchaseRequestId}`);
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
const db = require('../db-sqlite');
|
||||
const path = require('path');
|
||||
|
||||
describe('采购付款分离 - 数据库层测试', () => {
|
||||
beforeAll(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
});
|
||||
|
||||
describe('采购申请表 (purchase_requests)', () => {
|
||||
test('应该能创建采购申请', async () => {
|
||||
const result = await db.query(`
|
||||
INSERT INTO purchase_requests
|
||||
(request_code, project_id, applicant, request_date, expense_category, total_amount, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, ['PUR-TEST-001', 1, '测试申请人', '2026-03-25', 'material', 10000.00, 'pending']);
|
||||
expect(result.changes).toBe(1);
|
||||
});
|
||||
|
||||
test('采购申请编号应该唯一', async () => {
|
||||
try {
|
||||
await db.query(`
|
||||
INSERT INTO purchase_requests
|
||||
(request_code, project_id, applicant, request_date, expense_category, total_amount, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, ['PUR-TEST-001', 1, '测试申请人2', '2026-03-25', 'material', 20000.00, 'pending']);
|
||||
fail('应该抛出唯一约束错误');
|
||||
} catch (error) {
|
||||
expect(error.message).toContain('UNIQUE constraint failed');
|
||||
}
|
||||
});
|
||||
|
||||
test('应该能查询采购申请', async () => {
|
||||
const result = await db.query('SELECT * FROM purchase_requests WHERE request_code = ?', ['PUR-TEST-001']);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
expect(result[0].request_code).toBe('PUR-TEST-001');
|
||||
});
|
||||
});
|
||||
|
||||
describe('采购明细表 (purchase_request_items)', () => {
|
||||
let purchaseRequestId;
|
||||
|
||||
beforeAll(async () => {
|
||||
const result = await db.query(`
|
||||
INSERT INTO purchase_requests
|
||||
(request_code, project_id, applicant, request_date, expense_category, total_amount, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, ['PUR-TEST-002', 1, '测试申请人', '2026-03-25', 'material', 5000.00, 'pending']);
|
||||
purchaseRequestId = result.lastID;
|
||||
});
|
||||
|
||||
test('应该能创建采购明细', async () => {
|
||||
const result = await db.query(`
|
||||
INSERT INTO purchase_request_items
|
||||
(purchase_request_id, product_name, quantity, unit_price, total_price)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [purchaseRequestId, '测试商品', 10, 500.00, 5000.00]);
|
||||
expect(result.changes).toBe(1);
|
||||
});
|
||||
|
||||
test('应该能查询采购明细', async () => {
|
||||
const result = await db.query('SELECT * FROM purchase_request_items WHERE purchase_request_id = ?', [purchaseRequestId]);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
expect(result[0].product_name).toBe('测试商品');
|
||||
});
|
||||
});
|
||||
|
||||
describe('库存记录表 (inventory_records)', () => {
|
||||
test('应该能创建库存入库记录', async () => {
|
||||
const result = await db.query(`
|
||||
INSERT INTO inventory_records
|
||||
(record_type, product_id, quantity, unit_price, total_amount, record_date, operator)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, ['in', 1, 100, 10.00, 1000.00, '2026-03-25', '测试操作员']);
|
||||
expect(result.changes).toBe(1);
|
||||
});
|
||||
|
||||
test('应该能创建库存出库记录', async () => {
|
||||
const result = await db.query(`
|
||||
INSERT INTO inventory_records
|
||||
(record_type, project_id, product_id, quantity, unit_price, total_amount, record_date, operator)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, ['out', 1, 1, 50, 10.00, 500.00, '2026-03-25', '测试操作员']);
|
||||
expect(result.changes).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('付款申请表修改', () => {
|
||||
test('付款申请表应该有purchase_request_id字段', async () => {
|
||||
const result = await db.query("PRAGMA table_info(payment_requests)");
|
||||
const hasPurchaseRequestId = result.some(col => col.name === 'purchase_request_id');
|
||||
expect(hasPurchaseRequestId).toBe(true);
|
||||
});
|
||||
|
||||
test('付款申请表应该有payment_type字段', async () => {
|
||||
const result = await db.query("PRAGMA table_info(payment_requests)");
|
||||
const hasPaymentType = result.some(col => col.name === 'payment_type');
|
||||
expect(hasPaymentType).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
const request = require('supertest');
|
||||
const app = require('../final-backend');
|
||||
const db = require('../db-sqlite');
|
||||
|
||||
describe('核销申请流程测试', () => {
|
||||
let server;
|
||||
let advanceId;
|
||||
let verificationId1;
|
||||
let verificationId2;
|
||||
|
||||
beforeAll(async () => {
|
||||
server = app.listen(3006);
|
||||
|
||||
// 清理测试数据
|
||||
await db.query('DELETE FROM verifications WHERE 1=1');
|
||||
await db.query('DELETE FROM advances WHERE 1=1');
|
||||
|
||||
// 创建测试预支申请
|
||||
const advanceResult = await db.query(
|
||||
'INSERT INTO advances (user_id, project_id, amount, currency, reason, advance_date, advance_code, status, applicant, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[1, null, 10000, 'CNY', '测试预支', '2026-03-25', 'ADV-TEST', 'executed', '测试用户', '[]']
|
||||
);
|
||||
advanceId = advanceResult.lastID;
|
||||
|
||||
// 更新预支单状态为待核销
|
||||
await db.query('UPDATE advances SET status = ? WHERE id = ?', ['pending_verification', advanceId]);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
describe('预支单状态管理', () => {
|
||||
test('预支单初始状态应该是待核销', async () => {
|
||||
const result = await db.query('SELECT status FROM advances WHERE id = ?', [advanceId]);
|
||||
expect(result.rows[0].status).toBe('pending_verification');
|
||||
});
|
||||
});
|
||||
|
||||
describe('核销申请创建', () => {
|
||||
test('创建第一张核销申请(非结算)', async () => {
|
||||
const response = await request(server)
|
||||
.post('/api/verifications')
|
||||
.send({
|
||||
verification_date: '2026-03-25',
|
||||
advance_id: advanceId,
|
||||
advance_code: 'ADV-TEST',
|
||||
advance_amount: 10000,
|
||||
currency: 'CNY',
|
||||
reason: '测试核销1',
|
||||
detail_items: [{ description: '测试费用1', amount: 6000, category: 'accommodation' }],
|
||||
attachments: [],
|
||||
applicant: '测试用户',
|
||||
expense_type: 'company',
|
||||
project_id: null,
|
||||
settlement: false
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
verificationId1 = response.body.data.id;
|
||||
|
||||
// 检查预支单已核销金额是否更新
|
||||
const advanceResult = await db.query('SELECT total_reimbursed FROM advances WHERE id = ?', [advanceId]);
|
||||
expect(advanceResult.rows[0].total_reimbursed).toBe(6000);
|
||||
});
|
||||
|
||||
test('创建第二张核销申请(结算)', async () => {
|
||||
const response = await request(server)
|
||||
.post('/api/verifications')
|
||||
.send({
|
||||
verification_date: '2026-03-25',
|
||||
advance_id: advanceId,
|
||||
advance_code: 'ADV-TEST',
|
||||
advance_amount: 10000,
|
||||
currency: 'CNY',
|
||||
reason: '测试核销2',
|
||||
detail_items: [{ description: '测试费用2', amount: 5000, category: 'food' }],
|
||||
attachments: [],
|
||||
applicant: '测试用户',
|
||||
expense_type: 'company',
|
||||
project_id: null,
|
||||
settlement: true,
|
||||
settlement_amount: -1000
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
verificationId2 = response.body.data.id;
|
||||
|
||||
// 检查预支单已核销金额是否更新
|
||||
const advanceResult = await db.query('SELECT total_reimbursed FROM advances WHERE id = ?', [advanceId]);
|
||||
expect(advanceResult.rows[0].total_reimbursed).toBe(11000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('核销申请审批和执行', () => {
|
||||
test('审批第一张核销申请', async () => {
|
||||
const response = await request(server)
|
||||
.post(`/api/verifications/${verificationId1}/approve`)
|
||||
.send({ remark: '批准' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
});
|
||||
|
||||
test('执行第一张核销申请(非结算)', async () => {
|
||||
const response = await request(server)
|
||||
.post(`/api/verifications/${verificationId1}/execute`)
|
||||
.send({ execute_method: '银行转账', voucher_no: 'VOUCHER-001' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
|
||||
// 检查预支单状态应该变为部分核销
|
||||
const advanceResult = await db.query('SELECT status FROM advances WHERE id = ?', [advanceId]);
|
||||
expect(advanceResult.rows[0].status).toBe('partial_verification');
|
||||
});
|
||||
|
||||
test('审批第二张核销申请', async () => {
|
||||
const response = await request(server)
|
||||
.post(`/api/verifications/${verificationId2}/approve`)
|
||||
.send({ remark: '批准' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
});
|
||||
|
||||
test('执行第二张核销申请(结算)', async () => {
|
||||
const response = await request(server)
|
||||
.post(`/api/verifications/${verificationId2}/execute`)
|
||||
.send({ execute_method: '银行转账', voucher_no: 'VOUCHER-002' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
|
||||
// 检查预支单状态应该变为已完成
|
||||
const advanceResult = await db.query('SELECT status FROM advances WHERE id = ?', [advanceId]);
|
||||
expect(advanceResult.rows[0].status).toBe('completed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('核销申请退回', () => {
|
||||
test('创建测试核销申请', async () => {
|
||||
const response = await request(server)
|
||||
.post('/api/verifications')
|
||||
.send({
|
||||
verification_date: '2026-03-25',
|
||||
advance_id: advanceId,
|
||||
advance_code: 'ADV-TEST',
|
||||
advance_amount: 10000,
|
||||
currency: 'CNY',
|
||||
reason: '测试退回',
|
||||
detail_items: [{ description: '测试费用', amount: 2000, category: 'transportation' }],
|
||||
attachments: [],
|
||||
applicant: '测试用户',
|
||||
expense_type: 'company',
|
||||
project_id: null,
|
||||
settlement: false
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
const testVerificationId = response.body.data.id;
|
||||
|
||||
// 检查预支单已核销金额是否更新
|
||||
let advanceResult = await db.query('SELECT total_reimbursed FROM advances WHERE id = ?', [advanceId]);
|
||||
expect(advanceResult.rows[0].total_reimbursed).toBe(13000);
|
||||
|
||||
// 退回核销申请
|
||||
const rejectResponse = await request(server)
|
||||
.post(`/api/verifications/${testVerificationId}/reject`)
|
||||
.send({ remark: '退回' });
|
||||
|
||||
expect(rejectResponse.status).toBe(200);
|
||||
expect(rejectResponse.body.success).toBe(true);
|
||||
|
||||
// 检查预支单已核销金额是否恢复
|
||||
advanceResult = await db.query('SELECT total_reimbursed FROM advances WHERE id = ?', [advanceId]);
|
||||
expect(advanceResult.rows[0].total_reimbursed).toBe(11000);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
const request = require('supertest');
|
||||
const app = require('../final-backend');
|
||||
|
||||
describe('质保金数据正确性测试 - TDD Red/Green', () => {
|
||||
describe('项目详情API - 质保金数据', () => {
|
||||
test('GET /api/projects/:id - 应该返回正确的质保金比例(从合同表读取)', async () => {
|
||||
const response = await request(app).get('/api/projects/1');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toHaveProperty('warranty_percent');
|
||||
|
||||
// 质保比例应该从 project_contracts 表的 warranty_deposit_percentage 字段读取
|
||||
// 而不是硬编码为 5
|
||||
const warrantyPercent = parseFloat(response.body.data.warranty_percent);
|
||||
expect(warrantyPercent).toBeGreaterThan(0);
|
||||
expect(warrantyPercent).toBeLessThanOrEqual(100);
|
||||
});
|
||||
|
||||
test('GET /api/projects/:id - 质保金金额应该根据合同金额和比例正确计算', async () => {
|
||||
const response = await request(app).get('/api/projects/1');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
|
||||
const contractAmount = parseFloat(response.body.data.contract_amount || '0');
|
||||
const warrantyPercent = parseFloat(response.body.data.warranty_percent || '0');
|
||||
const warrantyAmount = parseFloat(response.body.data.warranty_amount || '0');
|
||||
|
||||
// 质保金金额 = 合同金额 * 质保比例 / 100
|
||||
const expectedWarrantyAmount = Math.round(contractAmount * warrantyPercent / 100);
|
||||
|
||||
// 允许1元的四舍五入误差
|
||||
expect(Math.abs(warrantyAmount - expectedWarrantyAmount)).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
test('GET /api/projects/:id - 质保期限应该从合同表读取', async () => {
|
||||
const response = await request(app).get('/api/projects/1');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toHaveProperty('warranty_months');
|
||||
|
||||
// 质保期限应该是数字且大于0
|
||||
const warrantyMonths = parseInt(response.body.data.warranty_months);
|
||||
expect(warrantyMonths).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('项目质保金列表API', () => {
|
||||
test('GET /api/projects/:id/warranty-deposits - 应该返回质保金记录', async () => {
|
||||
const response = await request(app).get('/api/projects/1/warranty-deposits');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('质保金数据字段正确性测试', () => {
|
||||
test('质保金数据应该包含所有必要字段', async () => {
|
||||
const response = await request(app).get('/api/projects/1');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
|
||||
const data = response.body.data;
|
||||
|
||||
// 检查所有质保金相关字段
|
||||
expect(data).toHaveProperty('has_warranty');
|
||||
expect(data).toHaveProperty('warranty_amount');
|
||||
expect(data).toHaveProperty('warranty_percent');
|
||||
expect(data).toHaveProperty('warranty_months');
|
||||
expect(data).toHaveProperty('warranty_start_date');
|
||||
expect(data).toHaveProperty('warranty_end_date');
|
||||
expect(data).toHaveProperty('warranty_status');
|
||||
});
|
||||
|
||||
test('质保比例和质保期限应该是不同的值', async () => {
|
||||
const response = await request(app).get('/api/projects/1');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
|
||||
const warrantyPercent = parseFloat(response.body.data.warranty_percent);
|
||||
const warrantyMonths = parseInt(response.body.data.warranty_months);
|
||||
|
||||
// 质保比例(百分比)和质保期限(月数)应该是不同的概念
|
||||
// 比例通常是 0-100 之间的数,期限通常是 12、24 等月数
|
||||
// 它们不应该相等(除非是极端情况,但测试中应该区分)
|
||||
expect(typeof warrantyPercent).toBe('number');
|
||||
expect(typeof warrantyMonths).toBe('number');
|
||||
expect(warrantyPercent).not.toBeNaN();
|
||||
expect(warrantyMonths).not.toBeNaN();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user