P1 通用查询:viewsets / app / menu / mail / gitea / mc / stats / sync_apps 等视图与命令的裸查询改委托 SDK Manager(Role/Company/Department/Position/Menu/Tenant/App)。 P2 App 体系:app_manager.py 与 app_manifests.py 实现零裸 ORM,职务多值/虚拟职务/公司下拉全部走 SDK。 P3 工具下沉:money/json_encoder/celery_utils/exception_handler/redis_lock 改为纯转发展开;ip_filter/storage 转发并注入 app.ini 配置源;common 下 DateTime/String/Naming/UUID 转发 SDK。 删除死代码 commission/strategies/engine.py(SDK 已有同物且零引用)与 payment/models.py 的 FromTenantAndPlatform;单测 patch 目标同步为 gvsdsdk.*,test_attack_detect fixture 补齐限频去重状态。
165 lines
5.5 KiB
Python
165 lines
5.5 KiB
Python
"""模块:money.py 单元测试
|
||
|
||
覆盖 ToDecimal / QuantizeYuan / YuanToFen / FenToYuan /
|
||
FormatYuan / AddYuan / MultiplyYuan / SafeDecimalOperation,
|
||
重点验证边界值、浮点规避、银行家舍入、非法输入降级。
|
||
"""
|
||
from decimal import Decimal, ROUND_HALF_UP
|
||
from django.test import SimpleTestCase
|
||
|
||
from gvsdsdk.money import (
|
||
ToDecimal, QuantizeYuan, YuanToFen, FenToYuan,
|
||
FormatYuan, AddYuan, MultiplyYuan, SafeDecimalOperation,
|
||
)
|
||
|
||
|
||
class ToDecimalTestCase(SimpleTestCase):
|
||
"""ToDecimal 安全转换。"""
|
||
|
||
def testToDecimal_None_ReturnsDefault(self):
|
||
self.assertEqual(ToDecimal(None), Decimal('0'))
|
||
|
||
def testToDecimal_EmptyString_ReturnsDefault(self):
|
||
self.assertEqual(ToDecimal(''), Decimal('0'))
|
||
|
||
def testToDecimal_Decimal_Passthrough(self):
|
||
d = Decimal('25.05')
|
||
self.assertIs(ToDecimal(d), d)
|
||
|
||
def testToDecimal_Float_ConvertsViaStr(self):
|
||
# 0.1 float 直接转 Decimal 会得到 0.1000000000000000055...
|
||
# 通过 str 转换避免精度污染
|
||
self.assertEqual(ToDecimal(0.1), Decimal('0.1'))
|
||
|
||
def testToDecimal_ValidString(self):
|
||
self.assertEqual(ToDecimal('25.005'), Decimal('25.005'))
|
||
|
||
def testToDecimal_Int(self):
|
||
self.assertEqual(ToDecimal(100), Decimal('100'))
|
||
|
||
def testToDecimal_InvalidString_ReturnsDefault(self):
|
||
self.assertEqual(ToDecimal('abc'), Decimal('0'))
|
||
|
||
def testToDecimal_InvalidString_CustomDefault(self):
|
||
self.assertEqual(ToDecimal('abc', default=Decimal('-1')), Decimal('-1'))
|
||
|
||
|
||
class QuantizeYuanTestCase(SimpleTestCase):
|
||
"""QuantizeYuan 银行家舍入。"""
|
||
|
||
def testQuantizeYuan_BankersRounding(self):
|
||
# 25.005 → 25.00(偶数舍入,0 是偶数)
|
||
self.assertEqual(QuantizeYuan('25.005'), Decimal('25.00'))
|
||
# 25.015 → 25.02(偶数舍入,2 是偶数)
|
||
self.assertEqual(QuantizeYuan('25.015'), Decimal('25.02'))
|
||
|
||
def testQuantizeYuan_HalfUpOverride(self):
|
||
self.assertEqual(
|
||
QuantizeYuan('25.005', rounding=ROUND_HALF_UP),
|
||
Decimal('25.01'),
|
||
)
|
||
|
||
def testQuantizeYuan_TruncatesExtraDigits(self):
|
||
self.assertEqual(QuantizeYuan('25.1'), Decimal('25.10'))
|
||
self.assertEqual(QuantizeYuan('25.123456'), Decimal('25.12'))
|
||
|
||
|
||
class YuanFenConversionTestCase(SimpleTestCase):
|
||
"""元/分互转。"""
|
||
|
||
def testYuanToFen_NormalValue(self):
|
||
self.assertEqual(YuanToFen('25.01'), 2501)
|
||
|
||
def testYuanToFen_RoundsBeforeConvert(self):
|
||
# 25.005 银行家舍入到 25.00 → 2500
|
||
self.assertEqual(YuanToFen('25.005'), 2500)
|
||
|
||
def testYuanToFen_Zero(self):
|
||
self.assertEqual(YuanToFen('0'), 0)
|
||
|
||
def testYuanToFen_Negative(self):
|
||
self.assertEqual(YuanToFen('-1.23'), -123)
|
||
|
||
def testFenToYuan_NormalValue(self):
|
||
self.assertEqual(FenToYuan(2501), Decimal('25.01'))
|
||
|
||
def testFenToYuan_Zero(self):
|
||
self.assertEqual(FenToYuan(0), Decimal('0.00'))
|
||
|
||
def testFenToYuan_Negative(self):
|
||
self.assertEqual(FenToYuan(-123), Decimal('-1.23'))
|
||
|
||
def testYuanFen_RoundTrip(self):
|
||
for yuan in ['0.01', '1.00', '99.99', '1000000.00']:
|
||
self.assertEqual(FenToYuan(YuanToFen(yuan)), Decimal(yuan))
|
||
|
||
|
||
class FormatYuanTestCase(SimpleTestCase):
|
||
"""FormatYuan 格式化。"""
|
||
|
||
def testFormatYuan_PlainString(self):
|
||
self.assertEqual(FormatYuan('25.1'), '25.10')
|
||
|
||
def testFormatYuan_WithComma(self):
|
||
self.assertEqual(FormatYuan('1234.5', with_comma=True), '1,234.50')
|
||
|
||
def testFormatYuan_LargeWithComma(self):
|
||
self.assertEqual(FormatYuan('1000000', with_comma=True), '1,000,000.00')
|
||
|
||
|
||
class AddYuanTestCase(SimpleTestCase):
|
||
"""AddYuan 多值求和。"""
|
||
|
||
def testAddYuan_EmptyArgs_ReturnsZero(self):
|
||
self.assertEqual(AddYuan(), Decimal('0.00'))
|
||
|
||
def testAddYuan_MultipleValues(self):
|
||
self.assertEqual(AddYuan('1.01', '2.02', '3.03'), Decimal('6.06'))
|
||
|
||
def testAddYuan_MixedTypes(self):
|
||
self.assertEqual(AddYuan(1, '0.50', Decimal('0.25')), Decimal('1.75'))
|
||
|
||
def testAddYuan_SkipsInvalid(self):
|
||
# None → default 0
|
||
self.assertEqual(AddYuan('1.00', None, 'abc'), Decimal('1.00'))
|
||
|
||
|
||
class MultiplyYuanTestCase(SimpleTestCase):
|
||
"""MultiplyYuan 单价×数量。"""
|
||
|
||
def testMultiplyYuan_IntegerQuantity(self):
|
||
self.assertEqual(MultiplyYuan('9.99', 3), Decimal('29.97'))
|
||
|
||
def testMultiplyYuan_DecimalQuantity(self):
|
||
self.assertEqual(MultiplyYuan('10.00', Decimal('2.5')), Decimal('25.00'))
|
||
|
||
def testMultiplyYuan_StringQuantity(self):
|
||
self.assertEqual(MultiplyYuan('100.00', '0.1'), Decimal('10.00'))
|
||
|
||
|
||
class SafeDecimalOperationTestCase(SimpleTestCase):
|
||
"""SafeDecimalOperation 装饰器。"""
|
||
|
||
def testSafeDecimalOperation_NormalReturn(self):
|
||
@SafeDecimalOperation(default=Decimal('-1'))
|
||
def Calc(a, b):
|
||
return ToDecimal(a) / ToDecimal(b)
|
||
|
||
self.assertEqual(Calc('10', '2'), Decimal('5'))
|
||
|
||
def testSafeDecimalOperation_ExceptionReturnsDefault(self):
|
||
@SafeDecimalOperation(default=Decimal('-1'))
|
||
def Calc(a, b):
|
||
return ToDecimal(a) / ToDecimal(b)
|
||
|
||
# 除零触发 ArithmeticError → 返回 default
|
||
self.assertEqual(Calc('10', '0'), Decimal('-1'))
|
||
|
||
def testSafeDecimalOperation_TypeErrorReturnsDefault(self):
|
||
@SafeDecimalOperation(default=Decimal('0'))
|
||
def Calc(value):
|
||
# Decimal + str 触发 TypeError
|
||
return Decimal('1') + value
|
||
|
||
self.assertEqual(Calc('not-a-number'), Decimal('0'))
|