test_mixin.py 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079
  1. # Copyright © 2023 Ingram Micro Inc. All rights reserved.
  2. from datetime import timedelta
  3. from time import sleep
  4. from uuid import uuid4
  5. import pytest
  6. from django.conf import settings
  7. from django.contrib.contenttypes.models import ContentType
  8. from django.db import transaction
  9. from django.db.models import CharField, F, IntegerField
  10. from django.utils.timezone import now
  11. from dj_cqrs.constants import (
  12. DEFAULT_MASTER_AUTO_UPDATE_FIELDS,
  13. DEFAULT_MASTER_MESSAGE_TTL,
  14. FIELDS_TRACKER_FIELD_NAME,
  15. SignalType,
  16. )
  17. from dj_cqrs.metas import MasterMeta
  18. from tests.dj_master import models
  19. from tests.dj_master.serializers import AuthorSerializer
  20. from tests.utils import (
  21. assert_is_sub_dict,
  22. assert_publisher_once_called_with_args,
  23. assert_tracked_fields,
  24. )
  25. class MasterMetaTest(MasterMeta):
  26. @classmethod
  27. def check_cqrs_fields(cls, model_cls):
  28. return cls._check_cqrs_fields(model_cls)
  29. @classmethod
  30. def check_correct_configuration(cls, model_cls):
  31. return cls._check_correct_configuration(model_cls)
  32. @classmethod
  33. def check_cqrs_tracked_fields(cls, model_cls):
  34. return cls._check_cqrs_tracked_fields(model_cls)
  35. def test_cqrs_fields_non_existing_field(mocker):
  36. with pytest.raises(AssertionError) as e:
  37. class Cls(object):
  38. CQRD_ID = 'ID'
  39. CQRS_FIELDS = ('char_field', 'integer_field')
  40. char_field = CharField(max_length=100, primary_key=True)
  41. int_field = IntegerField()
  42. _meta = mocker.MagicMock(concrete_fields=(char_field, int_field), private_fields=())
  43. _meta.pk.name = 'char_field'
  44. MasterMetaTest.check_cqrs_fields(Cls)
  45. assert str(e.value) == 'CQRS_FIELDS field is not correctly set for model Cls.'
  46. def test_cqrs_fields_id_is_not_included(mocker):
  47. with pytest.raises(AssertionError) as e:
  48. class Cls(object):
  49. CQRD_ID = 'ID'
  50. CQRS_FIELDS = ('int_field',)
  51. char_field = CharField(max_length=100, primary_key=True)
  52. int_field = IntegerField()
  53. _meta = mocker.MagicMock(concrete_fields=(char_field, int_field), private_fields=())
  54. _meta.pk.name = 'char_field'
  55. MasterMetaTest.check_cqrs_fields(Cls)
  56. assert str(e.value) == 'PK is not in CQRS_FIELDS for model Cls.'
  57. def test_cqrs_fields_duplicates(mocker):
  58. with pytest.raises(AssertionError) as e:
  59. class Cls(object):
  60. CQRD_ID = 'ID'
  61. CQRS_FIELDS = ('char_field', 'char_field')
  62. char_field = CharField(max_length=100, primary_key=True)
  63. int_field = IntegerField()
  64. _meta = mocker.MagicMock(concrete_fields=(char_field, int_field), private_fields=())
  65. _meta.pk.name = 'char_field'
  66. MasterMetaTest.check_cqrs_fields(Cls)
  67. assert str(e.value) == 'Duplicate names in CQRS_FIELDS field for model Cls.'
  68. def test_cqrs_bad_configuration(mocker):
  69. with pytest.raises(AssertionError) as e:
  70. class Cls(object):
  71. CQRD_ID = 'ID'
  72. CQRS_FIELDS = ('char_field',)
  73. CQRS_SERIALIZER = 'path.to.serializer'
  74. char_field = CharField(max_length=100, primary_key=True)
  75. _meta = mocker.MagicMock(concrete_fields=(char_field,), private_fields=())
  76. _meta.pk.name = 'char_field'
  77. MasterMetaTest.check_correct_configuration(Cls)
  78. assert "CQRS_FIELDS can't be set together with CQRS_SERIALIZER." in str(e.value)
  79. @pytest.mark.django_db
  80. def test_to_cqrs_dict_has_cqrs_fields():
  81. m = models.AutoFieldsModel.objects.create()
  82. dct = m.to_cqrs_dict()
  83. assert dct['cqrs_revision'] == 0 and dct['cqrs_updated'] is not None
  84. def test_to_cqrs_dict_basic_types():
  85. dt = now()
  86. uid = uuid4()
  87. m = models.BasicFieldsModel(
  88. int_field=1,
  89. bool_field=False,
  90. char_field='str',
  91. date_field=None,
  92. datetime_field=dt,
  93. float_field=1.23,
  94. url_field='http://example.com',
  95. uuid_field=uid,
  96. )
  97. assert_is_sub_dict(
  98. {
  99. 'int_field': 1,
  100. 'bool_field': False,
  101. 'char_field': 'str',
  102. 'date_field': None,
  103. 'datetime_field': str(dt),
  104. 'float_field': 1.23,
  105. 'url_field': 'http://example.com',
  106. 'uuid_field': str(uid),
  107. },
  108. m.to_cqrs_dict(),
  109. )
  110. def test_to_cqrs_dict_all_fields():
  111. m = models.AllFieldsModel(char_field='str')
  112. assert_is_sub_dict({'id': None, 'int_field': None, 'char_field': 'str'}, m.to_cqrs_dict())
  113. def test_to_cqrs_dict_chosen_fields():
  114. m = models.ChosenFieldsModel(float_field=1.23)
  115. assert_is_sub_dict({'char_field': None, 'id': None}, m.to_cqrs_dict())
  116. @pytest.mark.django_db
  117. def test_to_cqrs_dict_auto_fields():
  118. m = models.AutoFieldsModel()
  119. assert_is_sub_dict({'id': None, 'created': None, 'updated': None}, m.to_cqrs_dict())
  120. m.save()
  121. cqrs_dct = m.to_cqrs_dict()
  122. for key in ('id', 'created', 'updated'):
  123. assert cqrs_dct[key] is not None
  124. def test_cqrs_sync_not_created():
  125. m = models.BasicFieldsModel()
  126. assert not m.cqrs_sync()
  127. @pytest.mark.django_db
  128. @pytest.mark.skipif(settings.DB_ENGINE != 'sqlite', reason='sqlite only')
  129. def test_cqrs_sync_cant_refresh_model():
  130. m = models.SimplestModel.objects.create()
  131. assert not m.cqrs_sync()
  132. @pytest.mark.django_db(transaction=True, reset_sequences=True)
  133. def test_cqrs_sync_not_saved(mocker):
  134. m = models.ChosenFieldsModel.objects.create(char_field='old')
  135. m.name = 'new'
  136. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  137. assert m.cqrs_sync()
  138. assert_publisher_once_called_with_args(
  139. publisher_mock,
  140. SignalType.SYNC,
  141. models.ChosenFieldsModel.CQRS_ID,
  142. {'char_field': 'old', 'id': m.pk},
  143. m.pk,
  144. )
  145. @pytest.mark.django_db(transaction=True)
  146. def test_cqrs_sync(mocker):
  147. m = models.ChosenFieldsModel.objects.create(char_field='old')
  148. m.char_field = 'new'
  149. m.save(update_fields=['char_field'])
  150. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  151. assert m.cqrs_sync()
  152. assert_publisher_once_called_with_args(
  153. publisher_mock,
  154. SignalType.SYNC,
  155. models.ChosenFieldsModel.CQRS_ID,
  156. {'char_field': 'new', 'id': m.pk},
  157. m.pk,
  158. )
  159. @pytest.mark.django_db(transaction=True)
  160. def test_cqrs_sync_optimized_for_class_serialization(mocker, django_assert_num_queries):
  161. models.Author.objects.create(
  162. id=5,
  163. name='hi',
  164. publisher=models.Publisher.objects.create(id=1, name='pub'),
  165. )
  166. m = models.Author.relate_cqrs_serialization(models.Author.objects.all()).first()
  167. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  168. with django_assert_num_queries(0):
  169. assert m.cqrs_sync()
  170. assert_publisher_once_called_with_args(
  171. publisher_mock,
  172. SignalType.SYNC,
  173. models.Author.CQRS_ID,
  174. {'id': 5, 'name': 'hi', 'publisher': {'id': 1, 'name': 'pub'}},
  175. 5,
  176. )
  177. @pytest.mark.django_db(transaction=True)
  178. def test_is_sync_instance(mocker):
  179. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  180. out_instance = models.FilteredSimplestModel.objects.create(name='a')
  181. in_instance = models.FilteredSimplestModel.objects.create(name='title')
  182. instances = (out_instance, in_instance)
  183. for instance in instances:
  184. assert instance.cqrs_revision == 0
  185. assert_publisher_once_called_with_args(
  186. publisher_mock,
  187. SignalType.SAVE,
  188. models.FilteredSimplestModel.CQRS_ID,
  189. {'name': 'title', 'id': in_instance.pk},
  190. in_instance.pk,
  191. )
  192. publisher_mock.reset_mock()
  193. in_instance.name = 'longer title'
  194. for instance in instances:
  195. instance.save()
  196. instance.refresh_from_db()
  197. assert instance.cqrs_revision == 1
  198. assert_publisher_once_called_with_args(
  199. publisher_mock,
  200. SignalType.SAVE,
  201. models.FilteredSimplestModel.CQRS_ID,
  202. {'name': 'longer title', 'id': in_instance.pk},
  203. in_instance.pk,
  204. )
  205. publisher_mock.reset_mock()
  206. out_instance.name = 'long'
  207. in_instance.name = 's'
  208. for instance in instances:
  209. instance.save()
  210. instance.refresh_from_db()
  211. assert instance.cqrs_revision == 2
  212. assert_publisher_once_called_with_args(
  213. publisher_mock,
  214. SignalType.SAVE,
  215. models.FilteredSimplestModel.CQRS_ID,
  216. {'name': 'long', 'id': out_instance.pk},
  217. out_instance.pk,
  218. )
  219. publisher_mock.reset_mock()
  220. in_instance.delete()
  221. assert publisher_mock.call_count == 0
  222. out_instance.delete()
  223. assert publisher_mock.call_count == 1
  224. @pytest.mark.django_db
  225. def test_create():
  226. for _ in range(2):
  227. m = models.AutoFieldsModel.objects.create()
  228. assert m.cqrs_revision == 0
  229. assert m.cqrs_updated is not None
  230. @pytest.mark.django_db(transaction=True)
  231. def test_update():
  232. m = models.AutoFieldsModel.objects.create()
  233. cqrs_updated = m.cqrs_updated
  234. for i in range(1, 3):
  235. m.save()
  236. m.refresh_from_db()
  237. assert m.cqrs_revision == i
  238. assert m.cqrs_updated > cqrs_updated
  239. cqrs_updated = m.cqrs_updated
  240. @pytest.mark.django_db(transaction=True)
  241. def test_transaction_rollbacked(mocker):
  242. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  243. try:
  244. with transaction.atomic():
  245. models.BasicFieldsModel.objects.create(
  246. int_field=1,
  247. char_field='str',
  248. )
  249. raise ValueError
  250. except ValueError:
  251. publisher_mock.assert_not_called()
  252. @pytest.mark.django_db(transaction=True)
  253. def test_transaction_commited(mocker):
  254. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  255. with transaction.atomic():
  256. models.BasicFieldsModel.objects.create(
  257. int_field=1,
  258. char_field='str',
  259. )
  260. assert_publisher_once_called_with_args(
  261. publisher_mock,
  262. SignalType.SAVE,
  263. models.BasicFieldsModel.CQRS_ID,
  264. {'char_field': 'str', 'int_field': 1},
  265. 1,
  266. )
  267. @pytest.mark.django_db(transaction=True)
  268. def test_transaction_rollbacked_to_savepoint(mocker):
  269. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  270. with transaction.atomic():
  271. models.BasicFieldsModel.objects.create(
  272. int_field=1,
  273. char_field='str',
  274. )
  275. try:
  276. with transaction.atomic(savepoint=True):
  277. raise ValueError
  278. except ValueError:
  279. pass
  280. assert_publisher_once_called_with_args(
  281. publisher_mock,
  282. SignalType.SAVE,
  283. models.BasicFieldsModel.CQRS_ID,
  284. {'char_field': 'str', 'int_field': 1},
  285. 1,
  286. )
  287. @pytest.mark.django_db(transaction=True)
  288. def test_serialization_no_related_instance(mocker):
  289. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  290. models.Author.objects.create(id=1, name='author', publisher=None)
  291. assert_publisher_once_called_with_args(
  292. publisher_mock,
  293. SignalType.SAVE,
  294. models.Author.CQRS_ID,
  295. {
  296. 'id': 1,
  297. 'name': 'author',
  298. 'publisher': None,
  299. 'books': [],
  300. 'cqrs_revision': 0,
  301. },
  302. 1,
  303. )
  304. @pytest.mark.django_db(transaction=True)
  305. def test_save_serialization(mocker, django_assert_num_queries, django_v_trans_q_count_sup):
  306. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  307. # < Django 4.2
  308. # 0 - Transaction start (SQLite only)
  309. # 1 - Publisher
  310. # 2 - Author
  311. # 3-4 - Books
  312. # 5-6 - Serialization with prefetch_related
  313. query_counter = 6 + django_v_trans_q_count_sup
  314. if settings.DB_ENGINE == 'sqlite' and django_v_trans_q_count_sup == 0:
  315. query_counter = 7
  316. with django_assert_num_queries(query_counter):
  317. with transaction.atomic(savepoint=False):
  318. publisher = models.Publisher.objects.create(id=1, name='publisher')
  319. author = models.Author.objects.create(id=1, name='author', publisher=publisher)
  320. for index in range(1, 3):
  321. models.Book.objects.create(id=index, title=str(index), author=author)
  322. assert_publisher_once_called_with_args(
  323. publisher_mock,
  324. SignalType.SAVE,
  325. models.Author.CQRS_ID,
  326. {
  327. 'id': 1,
  328. 'name': 'author',
  329. 'publisher': {
  330. 'id': 1,
  331. 'name': 'publisher',
  332. },
  333. 'books': [
  334. {
  335. 'id': 1,
  336. 'name': '1',
  337. },
  338. {
  339. 'id': 2,
  340. 'name': '2',
  341. },
  342. ],
  343. 'cqrs_revision': 0,
  344. },
  345. 1,
  346. )
  347. @pytest.mark.django_db(transaction=True)
  348. def test_delete_serialization():
  349. m = models.Author.objects.create(id=1, name='author', publisher=None)
  350. assert models.Author.objects.count() == 1
  351. m.delete()
  352. assert models.Author.objects.count() == 0
  353. @pytest.mark.django_db(transaction=True)
  354. def test_create_from_related_table(mocker):
  355. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  356. author = models.Author.objects.create(id=1, name='author', publisher=None)
  357. with transaction.atomic():
  358. models.Book.objects.create(id=1, title='title', author=author)
  359. # Calling author.cqrs_sync() would result in a wrong cqrs_revision!
  360. author.save()
  361. assert publisher_mock.call_count == 2
  362. assert_is_sub_dict(
  363. {
  364. 'id': 1,
  365. 'name': 'author',
  366. 'publisher': None,
  367. 'books': [
  368. {
  369. 'id': 1,
  370. 'name': 'title',
  371. },
  372. ],
  373. 'cqrs_revision': 1,
  374. },
  375. publisher_mock.call_args[0][0].instance_data,
  376. )
  377. @pytest.mark.django_db(transaction=True)
  378. def test_update_from_related_table(mocker):
  379. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  380. with transaction.atomic():
  381. publisher = models.Publisher.objects.create(id=1, name='publisher')
  382. author = models.Author.objects.create(id=1, name='author', publisher=publisher)
  383. with transaction.atomic():
  384. publisher.name = 'new'
  385. publisher.save()
  386. author.save()
  387. assert publisher_mock.call_count == 2
  388. assert_is_sub_dict(
  389. {
  390. 'id': 1,
  391. 'name': 'author',
  392. 'publisher': {
  393. 'id': 1,
  394. 'name': 'new',
  395. },
  396. 'books': [],
  397. 'cqrs_revision': 1,
  398. },
  399. publisher_mock.call_args[0][0].instance_data,
  400. )
  401. @pytest.mark.django_db(transaction=True)
  402. def test_to_cqrs_dict_serializer_ok():
  403. model = models.Author.objects.create(id=1)
  404. assert_is_sub_dict(AuthorSerializer(model).data, model.to_cqrs_dict())
  405. assert models.Author._cqrs_serializer_class == AuthorSerializer
  406. @pytest.mark.django_db(transaction=True)
  407. def test_to_cqrs_dict_serializer_import_error():
  408. with pytest.raises(ImportError) as e:
  409. with transaction.atomic():
  410. models.BadSerializationClassModel.objects.create(id=1)
  411. assert "CQRS_SERIALIZER can't be imported." in str(e)
  412. @pytest.mark.django_db(transaction=True)
  413. def test_to_cqrs_dict_serializer_bad_related_function(caplog):
  414. models.BadQuerySetSerializationClassModel.objects.create()
  415. assert (
  416. "Can't produce message from master model 'BadQuerySetSerializationClassModel': "
  417. "The instance doesn't exist"
  418. ) in caplog.text
  419. @pytest.mark.django_db(transaction=True)
  420. def test_multiple_inheritance(mocker):
  421. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  422. models.NonMetaClassModel.objects.create(name='abc')
  423. assert publisher_mock.call_count == 1
  424. @pytest.mark.django_db(transaction=True)
  425. def test_non_sent(mocker):
  426. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  427. m = models.NonSentModel.objects.create()
  428. assert publisher_mock.call_count == 0
  429. m.refresh_from_db()
  430. assert m.cqrs_revision == 0
  431. m.save()
  432. assert publisher_mock.call_count == 0
  433. m.refresh_from_db()
  434. assert m.cqrs_revision == 1
  435. m.delete()
  436. assert publisher_mock.call_count == 0
  437. def test_cqrs_tracked_fields_non_existing_field(mocker):
  438. with pytest.raises(AssertionError) as e:
  439. class Cls(object):
  440. CQRD_ID = 'ID'
  441. CQRS_TRACKED_FIELDS = ('char_field', 'integer_field')
  442. char_field = CharField(max_length=100, primary_key=True)
  443. int_field = IntegerField()
  444. _meta = mocker.MagicMock(concrete_fields=(char_field, int_field), private_fields=())
  445. _meta.pk.name = 'char_field'
  446. MasterMetaTest.check_cqrs_tracked_fields(Cls)
  447. assert str(e.value) == 'CQRS_TRACKED_FIELDS field is not correctly set for model Cls.'
  448. def test_cqrs_tracked_fields_duplicates(mocker):
  449. with pytest.raises(AssertionError) as e:
  450. class Cls(object):
  451. CQRD_ID = 'ID'
  452. CQRS_TRACKED_FIELDS = ('char_field', 'char_field')
  453. char_field = CharField(max_length=100, primary_key=True)
  454. int_field = IntegerField()
  455. _meta = mocker.MagicMock(concrete_fields=(char_field, int_field), private_fields=())
  456. _meta.pk.name = 'char_field'
  457. MasterMetaTest.check_cqrs_tracked_fields(Cls)
  458. assert str(e.value) == 'Duplicate names in CQRS_TRACKED_FIELDS field for model Cls.'
  459. def test_cqrs_tracked_fields_bad_configuration(mocker):
  460. with pytest.raises(AssertionError) as e:
  461. class Cls(object):
  462. CQRD_ID = 'ID'
  463. CQRS_TRACKED_FIELDS = 'bad_config'
  464. char_field = CharField(max_length=100, primary_key=True)
  465. int_field = IntegerField()
  466. _meta = mocker.MagicMock(concrete_fields=(char_field, int_field), private_fields=())
  467. _meta.pk.name = 'char_field'
  468. MasterMetaTest.check_cqrs_tracked_fields(Cls)
  469. assert str(e.value) == 'Model Cls: Invalid configuration for CQRS_TRACKED_FIELDS'
  470. def test_cqrs_tracked_fields_model_has_tracker(mocker):
  471. instance = models.TrackedFieldsChildModel()
  472. tracker = getattr(instance, FIELDS_TRACKER_FIELD_NAME)
  473. assert tracker is not None
  474. def test_cqrs_tracked_fields_related_fields(mocker):
  475. instance = models.TrackedFieldsChildModel()
  476. tracker = getattr(instance, FIELDS_TRACKER_FIELD_NAME)
  477. assert_tracked_fields(models.TrackedFieldsChildModel, tracker.fields)
  478. def test_cqrs_tracked_fields_all_related_fields(mocker):
  479. instance = models.TrackedFieldsAllWithChildModel()
  480. tracker = getattr(instance, FIELDS_TRACKER_FIELD_NAME)
  481. assert_tracked_fields(models.TrackedFieldsAllWithChildModel, tracker.fields)
  482. @pytest.mark.django_db(transaction=True)
  483. def test_cqrs_tracked_fields_tracking(mocker):
  484. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  485. instance = models.TrackedFieldsParentModel()
  486. instance.char_field = 'Value'
  487. instance.save()
  488. tracked_data = instance.get_tracked_fields_data()
  489. assert publisher_mock.call_args[0][0].previous_data == tracked_data
  490. assert tracked_data == {'cqrs_revision': None, 'char_field': None}
  491. assert tracked_data is not None
  492. assert 'char_field' in tracked_data
  493. assert tracked_data['char_field'] is None
  494. instance.char_field = 'New Value'
  495. instance.save()
  496. tracked_data = instance.get_tracked_fields_data()
  497. assert 'char_field' in tracked_data
  498. assert tracked_data['char_field'] == 'Value'
  499. assert publisher_mock.call_args[0][0].previous_data == tracked_data
  500. assert tracked_data == {'cqrs_revision': 0, 'char_field': 'Value'}
  501. @pytest.mark.django_db(transaction=True)
  502. def test_cqrs_tracked_fields_date_and_datetime_tracking(mocker):
  503. old_dt = now()
  504. old_d = (old_dt + timedelta(days=1)).date()
  505. models.BasicFieldsModel.objects.create(
  506. int_field=1,
  507. datetime_field=old_dt,
  508. date_field=old_d,
  509. )
  510. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  511. instance = models.BasicFieldsModel.objects.first()
  512. instance.datetime_field = now()
  513. instance.date_field = now().date()
  514. instance.save()
  515. tracked_data = instance.get_tracked_fields_data()
  516. assert (
  517. publisher_mock.call_args[0][0].previous_data
  518. == tracked_data
  519. == {
  520. 'cqrs_revision': 0,
  521. 'datetime_field': str(old_dt),
  522. 'date_field': str(old_d),
  523. }
  524. )
  525. def test_mptt_cqrs_tracked_fields_model_has_tracker():
  526. instance = models.MPTTWithTrackingModel()
  527. tracker = getattr(instance, FIELDS_TRACKER_FIELD_NAME)
  528. assert tracker is not None
  529. def test_mptt_cqrs_tracked_fields_related_fields():
  530. instance = models.MPTTWithTrackingModel()
  531. tracker = getattr(instance, FIELDS_TRACKER_FIELD_NAME)
  532. assert_tracked_fields(models.MPTTWithTrackingModel, tracker.fields)
  533. @pytest.mark.django_db(transaction=True)
  534. def test_f_expr():
  535. m = models.AllFieldsModel.objects.create(int_field=0, char_field='char')
  536. m.int_field = F('int_field') + 1
  537. m.save()
  538. cqrs_data = m.to_cqrs_dict()
  539. previous_data = m.get_tracked_fields_data()
  540. assert 'int_field' in cqrs_data
  541. assert cqrs_data['int_field'] == 1
  542. assert 'int_field' in previous_data
  543. assert previous_data['int_field'] == 0
  544. @pytest.mark.django_db(transaction=True)
  545. def test_generic_fk():
  546. sm = models.SimplestModel.objects.create(id=1, name='char')
  547. m = models.WithGenericFKModel.objects.create(content_object=sm)
  548. ct = ContentType.objects.get_for_model(models.SimplestModel)
  549. cqrs_data = m.to_cqrs_dict()
  550. previous_data = m.get_tracked_fields_data()
  551. assert 'content_object' not in cqrs_data
  552. assert 'content_type' in cqrs_data
  553. assert 'object_id' in cqrs_data
  554. assert cqrs_data['object_id'] == sm.pk
  555. assert cqrs_data['content_type'] == ct.pk
  556. assert 'content_object' not in previous_data
  557. assert 'content_type' not in previous_data
  558. for prev_data_key in ('object_id', 'cqrs_revision', 'content_type_id'):
  559. assert previous_data[prev_data_key] is None
  560. sm1 = models.SimplestModel.objects.create(id=2, name='name')
  561. m.content_object = sm1
  562. m.save()
  563. previous_data = m.get_tracked_fields_data()
  564. assert 'content_object' not in previous_data
  565. assert 'content_type' not in previous_data
  566. assert previous_data['object_id'] == sm.pk
  567. @pytest.mark.django_db(transaction=True)
  568. def test_m2m_not_supported():
  569. m1 = models.M2MModel.objects.create(id=1, name='name')
  570. m2m = models.WithM2MModel.objects.create(char_field='test')
  571. m2m.m2m_field.add(m1)
  572. m2m.save()
  573. cqrs_data = m2m.to_cqrs_dict()
  574. assert 'm2m_field' not in cqrs_data
  575. assert 'char_field' in cqrs_data
  576. @pytest.mark.django_db(transaction=True)
  577. def test_transaction_instance_saved_once_simple_case(mocker):
  578. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  579. i0 = models.TrackedFieldsChildModel.objects.create(char_field='old')
  580. with transaction.atomic():
  581. i1 = models.TrackedFieldsParentModel.objects.create(char_field='1')
  582. i1.char_field = '2'
  583. i1.save()
  584. i2 = models.TrackedFieldsParentModel(char_field='a')
  585. i2.save()
  586. i3 = models.TrackedFieldsChildModel.objects.create(char_field='.')
  587. i0.char_field = 'new'
  588. i0.save()
  589. assert publisher_mock.call_count == 5
  590. for i in [i0, i1, i2, i3]:
  591. i.refresh_from_db()
  592. assert i0.cqrs_revision == 1
  593. assert i1.cqrs_revision == 0
  594. assert i2.cqrs_revision == 0
  595. assert i3.cqrs_revision == 0
  596. mapper = (
  597. (i0.pk, 0, 'old', None),
  598. (i1.pk, 0, '2', None),
  599. (i2.pk, 0, 'a', None),
  600. (i3.pk, 0, '.', None),
  601. (i0.pk, 1, 'new', 'old'),
  602. )
  603. for index, call in enumerate(publisher_mock.call_args_list):
  604. payload = call[0][0]
  605. expected_data = mapper[index]
  606. assert payload.pk == expected_data[0]
  607. assert payload.instance_data['cqrs_revision'] == expected_data[1]
  608. assert payload.instance_data['char_field'] == expected_data[2]
  609. assert payload.previous_data['char_field'] == expected_data[3]
  610. @pytest.mark.django_db(transaction=True)
  611. def test_transaction_instance_saved_multiple_times_previous_data(mocker):
  612. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  613. instance = models.TrackedFieldsParentModel.objects.create(char_field='db_value')
  614. with transaction.atomic():
  615. instance.refresh_from_db()
  616. instance.char_field = 'save_1'
  617. instance.save()
  618. instance.char_field = 'save_2'
  619. instance.save()
  620. assert publisher_mock.call_count == 2
  621. payload_create = publisher_mock.call_args_list[0][0][0]
  622. payload_update = publisher_mock.call_args_list[1][0][0]
  623. assert payload_create.instance_data['char_field'] == 'db_value'
  624. assert payload_create.previous_data['char_field'] is None
  625. assert payload_update.instance_data['char_field'] == 'save_2'
  626. assert payload_update.previous_data['char_field'] == 'db_value'
  627. @pytest.mark.django_db(transaction=True)
  628. def test_cqrs_saves_count_lifecycle():
  629. instance = models.TrackedFieldsParentModel(char_field='1')
  630. instance.reset_cqrs_saves_count()
  631. assert instance.cqrs_saves_count == 0
  632. assert instance.is_initial_cqrs_save
  633. instance.save()
  634. assert instance.cqrs_saves_count == 0
  635. assert instance.is_initial_cqrs_save
  636. instance.save()
  637. assert instance.cqrs_saves_count == 0
  638. assert instance.is_initial_cqrs_save
  639. instance.refresh_from_db()
  640. assert instance.cqrs_saves_count == 0
  641. assert instance.is_initial_cqrs_save
  642. with transaction.atomic():
  643. instance.save()
  644. assert instance.cqrs_saves_count == 1
  645. assert instance.is_initial_cqrs_save
  646. instance.save()
  647. assert instance.cqrs_saves_count == 2
  648. assert not instance.is_initial_cqrs_save
  649. instance.refresh_from_db()
  650. assert instance.cqrs_saves_count == 2
  651. assert not instance.is_initial_cqrs_save
  652. same_db_object_other_instance = models.TrackedFieldsParentModel.objects.first()
  653. assert same_db_object_other_instance.pk == instance.pk
  654. assert same_db_object_other_instance.cqrs_saves_count == 0
  655. assert same_db_object_other_instance.is_initial_cqrs_save
  656. same_db_object_other_instance.save()
  657. assert same_db_object_other_instance.cqrs_saves_count == 1
  658. assert same_db_object_other_instance.is_initial_cqrs_save
  659. same_db_object_other_instance.reset_cqrs_saves_count()
  660. assert same_db_object_other_instance.cqrs_saves_count == 0
  661. assert same_db_object_other_instance.is_initial_cqrs_save
  662. same_db_object_other_instance.save()
  663. assert same_db_object_other_instance.cqrs_saves_count == 1
  664. assert same_db_object_other_instance.is_initial_cqrs_save
  665. assert instance.cqrs_saves_count == 0
  666. assert same_db_object_other_instance.cqrs_saves_count == 0
  667. @pytest.mark.django_db(transaction=True)
  668. def test_sequential_transactions(mocker):
  669. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  670. with transaction.atomic():
  671. instance = models.TrackedFieldsParentModel.objects.create(char_field='1')
  672. with transaction.atomic():
  673. instance.char_field = '3'
  674. instance.save()
  675. transaction.set_rollback(True)
  676. instance.reset_cqrs_saves_count()
  677. with transaction.atomic():
  678. instance.char_field = '2'
  679. instance.save()
  680. instance.refresh_from_db()
  681. assert publisher_mock.call_count == 2
  682. assert instance.cqrs_revision == 1
  683. assert publisher_mock.call_args_list[0][0][0].instance_data['char_field'] == '1'
  684. assert publisher_mock.call_args_list[1][0][0].instance_data['char_field'] == '2'
  685. @pytest.mark.django_db(transaction=True)
  686. def test_get_custom_cqrs_delete_data(mocker):
  687. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  688. m = models.SimplestModel.objects.create(id=1)
  689. m.get_custom_cqrs_delete_data = lambda *args: {'1': '2'}
  690. m.delete()
  691. payload = publisher_mock.call_args_list[1][0][0]
  692. assert payload.signal_type == SignalType.DELETE
  693. assert payload.instance_data['id'] == 1
  694. assert payload.instance_data['cqrs_revision'] == 1
  695. assert payload.instance_data['custom'] == {'1': '2'}
  696. @pytest.mark.django_db(transaction=True)
  697. def test_save_update_fields_no_cqrs_fields_default_behavior(mocker):
  698. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  699. instance = models.SimplestModel.objects.create(id=1)
  700. publisher_mock.reset_mock()
  701. instance.name = 'New'
  702. instance.save(update_fields=['name'])
  703. instance.refresh_from_db()
  704. assert publisher_mock.call_count == 0
  705. assert instance.cqrs_revision == 0
  706. assert instance.name == 'New'
  707. @pytest.mark.django_db(transaction=True)
  708. def test_save_update_fields_no_cqrs_fields_global_flag_changed(mocker, settings):
  709. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  710. instance = models.SimplestModel.objects.create(id=1)
  711. previous_cqrs_updated = instance.cqrs_updated
  712. publisher_mock.reset_mock()
  713. sleep(0.1)
  714. settings.CQRS = {
  715. 'transport': 'tests.dj.transport.TransportStub',
  716. 'master': {
  717. 'CQRS_AUTO_UPDATE_FIELDS': not DEFAULT_MASTER_AUTO_UPDATE_FIELDS,
  718. 'CQRS_MESSAGE_TTL': DEFAULT_MASTER_MESSAGE_TTL,
  719. 'correlation_function': None,
  720. 'meta_function': None,
  721. },
  722. }
  723. instance.name = 'New'
  724. instance.save(update_fields=['name'])
  725. assert_publisher_once_called_with_args(
  726. publisher_mock,
  727. SignalType.SAVE,
  728. models.SimplestModel.CQRS_ID,
  729. {'id': 1, 'name': 'New', 'cqrs_revision': 1},
  730. 1,
  731. )
  732. assert instance.cqrs_updated > previous_cqrs_updated
  733. instance.refresh_from_db()
  734. assert instance.name == 'New'
  735. @pytest.mark.django_db(transaction=True)
  736. def test_save_update_fields_with_cqrs_fields(mocker):
  737. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  738. instance = models.SimplestModel.objects.create(id=1, name='Old')
  739. previous_cqrs_updated = instance.cqrs_updated
  740. publisher_mock.reset_mock()
  741. sleep(0.1)
  742. instance.name = 'New'
  743. instance.cqrs_revision = F('cqrs_revision') + 1
  744. instance.save(update_fields=['name', 'cqrs_revision', 'cqrs_updated'], update_cqrs_fields=False)
  745. assert_publisher_once_called_with_args(
  746. publisher_mock,
  747. SignalType.SAVE,
  748. models.SimplestModel.CQRS_ID,
  749. {'id': 1, 'name': 'New', 'cqrs_revision': 1},
  750. 1,
  751. )
  752. assert instance.cqrs_updated > previous_cqrs_updated
  753. instance.refresh_from_db()
  754. assert instance.name == 'New'
  755. @pytest.mark.django_db(transaction=True)
  756. def test_save_update_fields_with_update_cqrs_fields_flag(mocker):
  757. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  758. instance = models.SimplestModel.objects.create(id=1)
  759. previous_cqrs_updated = instance.cqrs_updated
  760. publisher_mock.reset_mock()
  761. sleep(0.1)
  762. instance.name = 'New'
  763. instance.save(update_fields=['name'], update_cqrs_fields=True)
  764. assert_publisher_once_called_with_args(
  765. publisher_mock,
  766. SignalType.SAVE,
  767. models.SimplestModel.CQRS_ID,
  768. {'id': 1, 'name': 'New', 'cqrs_revision': 1},
  769. 1,
  770. )
  771. assert instance.cqrs_updated > previous_cqrs_updated
  772. instance.refresh_from_db()
  773. assert instance.name == 'New'
  774. @pytest.mark.django_db(transaction=True)
  775. def test_get_cqrs_meta_global_meta_function(mocker, settings):
  776. f = mocker.MagicMock()
  777. f.return_value = {1: 2}
  778. settings.CQRS['master']['meta_function'] = f
  779. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  780. obj = models.SimplestModel.objects.create(id=1)
  781. assert publisher_mock.call_args[0][0].meta == {1: 2}
  782. f.assert_called_once_with(
  783. obj=obj,
  784. instance_data={
  785. 'cqrs_revision': 0,
  786. 'cqrs_updated': str(obj.cqrs_updated),
  787. 'id': 1,
  788. 'name': None,
  789. },
  790. previous_data=None,
  791. signal_type=SignalType.SAVE,
  792. )
  793. @pytest.mark.django_db(transaction=True)
  794. def test_get_cqrs_meta_custom_function(mocker, settings):
  795. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  796. obj = models.SimplestModel.objects.create(id=1)
  797. publisher_mock.reset_mock()
  798. obj.get_cqrs_meta = lambda **k: {'a': {'b': {}}}
  799. f = mocker.MagicMock()
  800. settings.CQRS['master']['meta_function'] = f
  801. obj.delete()
  802. assert publisher_mock.call_args[0][0].meta == {'a': {'b': {}}}
  803. f.assert_not_called()
  804. @pytest.mark.django_db(transaction=True)
  805. def test_get_cqrs_meta_default(mocker, settings):
  806. publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
  807. obj = models.SimplestModel.objects.create(id=1)
  808. publisher_mock.reset_mock()
  809. obj.save()
  810. assert publisher_mock.call_args[0][0].meta == {}