first commit
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:qiangbao_accounting/models/account.dart';
|
||||
|
||||
void main() {
|
||||
group('余额反算 initialBalanceFor', () {
|
||||
// 后端公式:balance = initialBalance + totalIncome - totalExpense
|
||||
test('无收支时目标余额就是期初余额', () {
|
||||
expect(
|
||||
initialBalanceFor(targetBalance: 5000, totalIncome: 0, totalExpense: 0),
|
||||
5000,
|
||||
);
|
||||
});
|
||||
|
||||
test('有收入时需扣除收入', () {
|
||||
// 收入 8000,想让余额显示 5000 → 期初 = 5000 - 8000 = -3000
|
||||
expect(
|
||||
initialBalanceFor(targetBalance: 5000, totalIncome: 8000, totalExpense: 0),
|
||||
-3000,
|
||||
);
|
||||
});
|
||||
|
||||
test('有支出时需加回支出', () {
|
||||
// 支出 1200,想让余额显示 5000 → 期初 = 5000 + 1200 = 6200
|
||||
expect(
|
||||
initialBalanceFor(targetBalance: 5000, totalIncome: 0, totalExpense: 1200),
|
||||
6200,
|
||||
);
|
||||
});
|
||||
|
||||
test('收支都有时综合换算', () {
|
||||
// 收 8000 支 1200,目标 5000 → 5000 - 8000 + 1200 = -1800
|
||||
expect(
|
||||
initialBalanceFor(targetBalance: 5000, totalIncome: 8000, totalExpense: 1200),
|
||||
-1800,
|
||||
);
|
||||
});
|
||||
|
||||
test('换算后余额能被后端公式还原(往返一致)', () {
|
||||
const target = 12345.67;
|
||||
const income = 8000.0;
|
||||
const expense = 3456.78;
|
||||
final initial = initialBalanceFor(
|
||||
targetBalance: target,
|
||||
totalIncome: income,
|
||||
totalExpense: expense,
|
||||
);
|
||||
// 模拟后端计算
|
||||
final balance = initial + income - expense;
|
||||
expect(balance, closeTo(target, 0.001));
|
||||
});
|
||||
|
||||
test('目标余额非法时返回 0', () {
|
||||
expect(
|
||||
initialBalanceFor(targetBalance: double.nan, totalIncome: 100, totalExpense: 0),
|
||||
0,
|
||||
);
|
||||
expect(
|
||||
initialBalanceFor(targetBalance: double.infinity, totalIncome: 0, totalExpense: 0),
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('AccountInfo.fromJson 防御性', () {
|
||||
test('字段缺失时不抛异常并按公式兜底算余额', () {
|
||||
final a = AccountInfo.fromJson({
|
||||
'id': '3',
|
||||
'initialBalance': '1000',
|
||||
'totalIncome': 500,
|
||||
'totalExpense': '200.5',
|
||||
// name / balance 缺失
|
||||
});
|
||||
expect(a.id, 3);
|
||||
expect(a.name, '默认账户');
|
||||
expect(a.initialBalance, 1000);
|
||||
expect(a.totalIncome, 500);
|
||||
expect(a.totalExpense, 200.5);
|
||||
expect(a.balance, 1000 + 500 - 200.5); // 本地兜底计算
|
||||
});
|
||||
|
||||
test('后端给了 balance 时优先使用后端值', () {
|
||||
final a = AccountInfo.fromJson({
|
||||
'initialBalance': 1000,
|
||||
'totalIncome': 500,
|
||||
'totalExpense': 200,
|
||||
'balance': 9999, // 故意与公式不一致
|
||||
});
|
||||
expect(a.balance, 9999);
|
||||
});
|
||||
|
||||
test('完全空对象返回零值账户', () {
|
||||
final a = AccountInfo.fromJson({});
|
||||
expect(a.balance, 0);
|
||||
expect(a.totalIncome, 0);
|
||||
expect(a.totalExpense, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:qiangbao_accounting/models/bill.dart';
|
||||
import 'package:qiangbao_accounting/utils/amount_util.dart';
|
||||
import 'package:qiangbao_accounting/utils/bill_grouping.dart';
|
||||
import 'package:qiangbao_accounting/utils/date_util.dart';
|
||||
|
||||
Bill bill({
|
||||
required int id,
|
||||
required String date,
|
||||
required double amount,
|
||||
required int type,
|
||||
String description = '',
|
||||
}) =>
|
||||
Bill(
|
||||
id: id,
|
||||
categoryId: 1,
|
||||
categoryName: '餐饮',
|
||||
categoryIcon: '🍔',
|
||||
amount: amount,
|
||||
description: description,
|
||||
billDate: date,
|
||||
type: type,
|
||||
);
|
||||
|
||||
void main() {
|
||||
group('月份/周界工具', () {
|
||||
test('monthStart 取当月 1 日', () {
|
||||
expect(formatDate(monthStart(DateTime(2026, 9, 21))), '2026-09-01');
|
||||
});
|
||||
|
||||
test('monthEnd 处理 28/30/31 天与闰年 2 月', () {
|
||||
expect(formatDate(monthEnd(DateTime(2026, 9, 5))), '2026-09-30');
|
||||
expect(formatDate(monthEnd(DateTime(2026, 2, 5))), '2026-02-28');
|
||||
expect(formatDate(monthEnd(DateTime(2028, 2, 5))), '2028-02-29'); // 闰年
|
||||
expect(formatDate(monthEnd(DateTime(2026, 12, 5))), '2026-12-31');
|
||||
});
|
||||
|
||||
test('shiftMonth 跨年正确', () {
|
||||
expect(formatDate(shiftMonth(DateTime(2026, 1, 1), -1)), '2025-12-01');
|
||||
expect(formatDate(shiftMonth(DateTime(2026, 12, 1), 1)), '2027-01-01');
|
||||
});
|
||||
|
||||
test('weekStart 返回本周一', () {
|
||||
// 2026-09-21 是周一
|
||||
expect(formatDate(weekStart(DateTime(2026, 9, 21))), '2026-09-21');
|
||||
// 周日应回到本周一(9/20 是周日 → 9/14)
|
||||
expect(formatDate(weekStart(DateTime(2026, 9, 20))), '2026-09-14');
|
||||
// 跨月:2026-10-01 是周四 → 本周一 9/28
|
||||
expect(formatDate(weekStart(DateTime(2026, 10, 1))), '2026-09-28');
|
||||
});
|
||||
|
||||
test('isSameMonth', () {
|
||||
expect(isSameMonth(DateTime(2026, 9, 1), DateTime(2026, 9, 30)), isTrue);
|
||||
expect(isSameMonth(DateTime(2026, 9, 1), DateTime(2026, 10, 1)), isFalse);
|
||||
});
|
||||
|
||||
test('parseBillDate 兼容 LocalDateTime', () {
|
||||
expect(parseBillDate('2026-09-21T12:30:00'), '2026-09-21');
|
||||
expect(parseBillDate('2026-09-21 12:30:00'), '2026-09-21');
|
||||
expect(parseBillDate('2026-09-21'), '2026-09-21');
|
||||
});
|
||||
});
|
||||
|
||||
group('金额工具', () {
|
||||
test('formatAmount 两位小数并挡掉非法值', () {
|
||||
expect(formatAmount(12.5), '12.50');
|
||||
expect(formatAmount(null), '0.00');
|
||||
expect(formatAmount(double.nan), '0.00');
|
||||
expect(formatAmount(double.infinity), '0.00');
|
||||
});
|
||||
|
||||
test('formatSignedAmount 按类型给符号(不看金额正负)', () {
|
||||
expect(formatSignedAmount(12.0, isExpense: true), '-¥12.00');
|
||||
expect(formatSignedAmount(12.0, isExpense: false), '+¥12.00');
|
||||
});
|
||||
|
||||
test('percentOf 防除零', () {
|
||||
expect(percentOf(25, 100), 25);
|
||||
expect(percentOf(25, 0), 0);
|
||||
expect(percentOf(0, 0), 0);
|
||||
});
|
||||
});
|
||||
|
||||
group('账单分组', () {
|
||||
test('按日期分组并分别累计支出/收入', () {
|
||||
final groups = groupBillsByDate([
|
||||
bill(id: 1, date: '2026-09-21', amount: 30, type: 1),
|
||||
bill(id: 2, date: '2026-09-21', amount: 20, type: 1),
|
||||
bill(id: 3, date: '2026-09-21', amount: 5000, type: 2),
|
||||
bill(id: 4, date: '2026-09-20', amount: 15.5, type: 1),
|
||||
]);
|
||||
|
||||
expect(groups.length, 2);
|
||||
// 保持输入顺序(后端已按日期倒序)
|
||||
expect(groups[0].date, '2026-09-21');
|
||||
expect(groups[0].bills.length, 3);
|
||||
expect(groups[0].expenseTotal, 50);
|
||||
expect(groups[0].incomeTotal, 5000);
|
||||
// 同日内不重排,保持后端 createTime 倒序
|
||||
expect(groups[0].bills.map((b) => b.id).toList(), [1, 2, 3]);
|
||||
|
||||
expect(groups[1].date, '2026-09-20');
|
||||
expect(groups[1].expenseTotal, 15.5);
|
||||
expect(groups[1].incomeTotal, 0);
|
||||
});
|
||||
|
||||
test('空列表返回空分组', () {
|
||||
expect(groupBillsByDate([]), isEmpty);
|
||||
});
|
||||
|
||||
test('金额恒为正数时按 type 判断收支方向', () {
|
||||
final expense = bill(id: 1, date: '2026-09-21', amount: 30, type: 1);
|
||||
final income = bill(id: 2, date: '2026-09-21', amount: 30, type: 2);
|
||||
expect(expense.isExpense, isTrue);
|
||||
expect(income.isExpense, isFalse);
|
||||
expect(expense.absAmount, 30);
|
||||
});
|
||||
});
|
||||
|
||||
group('Bill.fromJson 防御性', () {
|
||||
test('字段缺失/类型不符不抛异常', () {
|
||||
final b = Bill.fromJson({
|
||||
'id': '12', // 字符串数字
|
||||
'amount': '35.5', // 字符串金额
|
||||
'billDate': '2026-09-21T10:00:00',
|
||||
// categoryName / categoryIcon / type 缺失
|
||||
});
|
||||
expect(b.id, 12);
|
||||
expect(b.amount, 35.5);
|
||||
expect(b.billDate, '2026-09-21');
|
||||
expect(b.categoryName, '未分类');
|
||||
expect(b.categoryIcon, '📦');
|
||||
expect(b.type, 1); // 缺省按支出
|
||||
});
|
||||
|
||||
test('type 为 2 时是收入', () {
|
||||
final b = Bill.fromJson({'id': 1, 'amount': 100, 'billDate': '2026-09-21', 'type': 2});
|
||||
expect(b.isExpense, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:qiangbao_accounting/utils/crypto.dart';
|
||||
import 'package:qiangbao_accounting/utils/jwt_util.dart';
|
||||
|
||||
/// 构造一个只含 payload 的假 JWT(本地 exp 判断不校验签名)
|
||||
String fakeJwt({DateTime? expiredAt, bool withExp = true}) {
|
||||
String seg(Map<String, dynamic> m) =>
|
||||
base64Url.encode(utf8.encode(jsonEncode(m))).replaceAll('=', '');
|
||||
final header = seg({'alg': 'HS512'});
|
||||
final payload = seg({
|
||||
'sub': 'tester',
|
||||
if (withExp)
|
||||
'exp': (expiredAt ?? DateTime.now().add(const Duration(hours: 1)))
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000,
|
||||
});
|
||||
return '$header.$payload.signature';
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('crypto encrypt/decrypt roundtrip 与 uniApp 端算法一致', () {
|
||||
const password = 'abc12345';
|
||||
final encrypted = encryptPassword(password);
|
||||
expect(encrypted, isNotEmpty);
|
||||
expect(encrypted, isNot(password));
|
||||
expect(decryptPassword(encrypted), password);
|
||||
});
|
||||
|
||||
group('JWT 过期判断', () {
|
||||
test('未来的 exp 视为未过期', () {
|
||||
expect(
|
||||
isJwtExpired(fakeJwt(expiredAt: DateTime.now().add(const Duration(hours: 1)))),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('过去的 exp 视为已过期', () {
|
||||
expect(
|
||||
isJwtExpired(fakeJwt(expiredAt: DateTime.now().subtract(const Duration(hours: 1)))),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('即将过期(落在 30 秒余量内)视为已过期', () {
|
||||
expect(
|
||||
isJwtExpired(fakeJwt(expiredAt: DateTime.now().add(const Duration(seconds: 5)))),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('无 exp 字段不做本地判断,避免误判', () {
|
||||
expect(isJwtExpired(fakeJwt(withExp: false)), isFalse);
|
||||
});
|
||||
|
||||
test('非 JWT 格式不做本地判断;空 token 视为无效', () {
|
||||
expect(isJwtExpired('not-a-jwt'), isFalse);
|
||||
expect(isJwtExpired(''), isTrue);
|
||||
expect(isJwtExpired(null), isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user