121 lines
5.8 KiB
Python
121 lines
5.8 KiB
Python
"""Keep production numbers linked to one order, including date-only history."""
|
|
import unittest
|
|
from datetime import datetime
|
|
from unittest import mock
|
|
from flask import Flask
|
|
|
|
import routes.purchase as purchase
|
|
|
|
|
|
class BackupProductionNoSyncTest(unittest.TestCase):
|
|
def test_exact_time_wins_without_using_date_fallback(self):
|
|
read = mock.Mock(return_value=[{'id': 9}])
|
|
self.assertEqual(purchase._backup_linked_order_ids(
|
|
read, 'domestic_orders', 'P1', '2026-09-27 18:29:13'), [9])
|
|
read.assert_called_once()
|
|
self.assertEqual(read.call_args.args[1], ('P1', '2026-09-27 18:29:13'))
|
|
|
|
def test_legacy_date_matches_only_unique_backup_and_product_order(self):
|
|
read = mock.Mock(side_effect=[[], [{'id': 2804}], [{'id': 1555}]])
|
|
self.assertEqual(purchase._backup_linked_order_ids(
|
|
read, 'domestic_orders', 'P1', '2026-09-27 18:29:13'), [2804])
|
|
self.assertIn('TRIM(`计划下单时间`)=%s', read.call_args_list[1].args[0])
|
|
self.assertEqual(read.call_args_list[1].args[1], ('P1', '2026-09-27'))
|
|
|
|
def test_ambiguous_same_day_backups_are_never_broadly_updated(self):
|
|
read = mock.Mock(side_effect=[[], [{'id': 9}], [{'id': 1}, {'id': 2}]])
|
|
with self.assertRaises(ValueError):
|
|
purchase._backup_linked_order_ids(read, 'domestic_orders', 'P1', '2026-09-27 18:29:13')
|
|
|
|
def test_ambiguous_legacy_product_orders_are_rejected(self):
|
|
read = mock.Mock(side_effect=[[], [{'id': 9}, {'id': 10}], [{'id': 1}]])
|
|
with self.assertRaises(ValueError):
|
|
purchase._backup_linked_order_ids(read, 'overseas_orders', 'P1', '2026-09-27 18:29:13')
|
|
|
|
def test_multiple_packaging_rows_can_belong_to_one_backup(self):
|
|
read = mock.Mock(side_effect=[[], [{'id': 9}, {'id': 10}], [{'id': 1}]])
|
|
self.assertEqual(purchase._backup_linked_order_ids(
|
|
read, 'packaging_orders', 'P1', '2026-09-27 18:29:13'), [9, 10])
|
|
|
|
def call_save(self, replies, execute_error=None):
|
|
app = Flask(__name__)
|
|
app.secret_key = 'test-only'
|
|
conn = mock.MagicMock()
|
|
cur = conn.cursor.return_value.__enter__.return_value
|
|
cur.fetchall.side_effect = replies
|
|
if execute_error:
|
|
cur.execute.side_effect = execute_error
|
|
with app.test_request_context(json={
|
|
'product_code': 'P1', 'backup_id': 1555, 'production_order_no': 'PS2026093029'
|
|
}), mock.patch.object(purchase, '_require_purchase_operation', return_value=None), \
|
|
mock.patch.object(purchase, 'get_db', return_value=conn), \
|
|
mock.patch.object(purchase, 'add_operation_log'):
|
|
response = purchase.backup_update_prod_no.__wrapped__()
|
|
return response.get_json(), conn, cur
|
|
|
|
def test_save_and_sync_commit_together_using_target_ids(self):
|
|
body, conn, cur = self.call_save([
|
|
[{'id': 1555, 'ordered_at': datetime(2026, 9, 27, 18, 29, 13)}],
|
|
[], [{'id': 2804}], [{'id': 1555}], # domestic legacy date
|
|
[], [], # no overseas order
|
|
[{'id': 3}, {'id': 4}], # exact packaging rows
|
|
])
|
|
self.assertEqual(body['code'], 0)
|
|
self.assertEqual(body['data']['synced'], {
|
|
'domestic_orders': 1, 'overseas_orders': 0, 'packaging_orders': 2,
|
|
})
|
|
writes = [call.args for call in cur.execute.call_args_list if call.args[0].startswith('UPDATE')]
|
|
self.assertEqual([args[1] for args in writes], [
|
|
('PS2026093029', 1555), ('PS2026093029', 2804), ('PS2026093029', 3, 4)
|
|
])
|
|
for sql, params in writes:
|
|
self.assertEqual(sql.count('%s'), len(params))
|
|
conn.commit.assert_called_once()
|
|
conn.rollback.assert_not_called()
|
|
conn.close.assert_called_once()
|
|
|
|
def test_ambiguity_prevents_even_the_backup_write(self):
|
|
body, conn, cur = self.call_save([
|
|
[{'id': 1555, 'ordered_at': datetime(2026, 9, 27, 18, 29, 13)}],
|
|
[], [{'id': 2804}], [{'id': 1555}, {'id': 1556}],
|
|
])
|
|
self.assertEqual(body['code'], 409)
|
|
self.assertFalse(any(call.args[0].startswith('UPDATE') for call in cur.execute.call_args_list))
|
|
conn.commit.assert_not_called()
|
|
conn.rollback.assert_called_once()
|
|
|
|
def test_wrong_product_or_missing_backup_is_not_saved(self):
|
|
body, conn, cur = self.call_save([[]])
|
|
self.assertEqual(body['code'], 404)
|
|
self.assertIn('product_code=%s', cur.execute.call_args.args[0])
|
|
self.assertEqual(cur.execute.call_args.args[1], (1555, 'P1'))
|
|
conn.commit.assert_not_called()
|
|
conn.rollback.assert_called_once()
|
|
|
|
def test_write_failure_rolls_back_all_related_updates(self):
|
|
app = Flask(__name__)
|
|
conn = mock.MagicMock()
|
|
cur = conn.cursor.return_value.__enter__.return_value
|
|
cur.fetchall.side_effect = [
|
|
[{'id': 1555, 'ordered_at': datetime(2026, 9, 27, 18, 29, 13)}],
|
|
[{'id': 2804}], [], [], [], [],
|
|
]
|
|
def execute(sql, params):
|
|
if sql.startswith('UPDATE domestic_orders'):
|
|
raise RuntimeError('database write failure')
|
|
cur.execute.side_effect = execute
|
|
with app.test_request_context(json={'product_code': 'P1', 'backup_id': 1555, 'production_order_no': 'PS1'}), \
|
|
mock.patch.object(purchase, '_require_purchase_operation', return_value=None), \
|
|
mock.patch.object(purchase, 'get_db', return_value=conn), \
|
|
mock.patch.object(purchase, 'add_operation_log') as audit:
|
|
with self.assertRaises(RuntimeError):
|
|
purchase.backup_update_prod_no.__wrapped__()
|
|
audit.assert_not_called()
|
|
conn.commit.assert_not_called()
|
|
conn.rollback.assert_called_once()
|
|
conn.close.assert_called_once()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|