|
11 | 11 | from netbox_diode_plugin.api.common import UnresolvedReference |
12 | 12 | from netbox_diode_plugin.api.matcher import ( |
13 | 13 | _find_obj_cache_key, |
| 14 | + _find_obj_rev_key, |
14 | 15 | find_existing_object, |
15 | 16 | invalidate_find_obj_entry, |
16 | 17 | ) |
@@ -292,3 +293,154 @@ def test_invalidate_does_not_affect_other_entries(self, _mock_ttl): |
292 | 293 | # Main should be gone |
293 | 294 | cache_key_main = _find_obj_cache_key(data_main, "dcim.manufacturer") |
294 | 295 | self.assertIsNone(django_cache.get(cache_key_main)) |
| 296 | + |
| 297 | + |
| 298 | +BRANCH_SCHEMA_MOCK = "netbox_diode_plugin.api.matcher._get_active_branch_schema" |
| 299 | + |
| 300 | + |
| 301 | +class BranchAwareCacheKeyTestCase(TestCase): |
| 302 | + """Tests that cache keys are isolated per branch.""" |
| 303 | + |
| 304 | + def setUp(self): |
| 305 | + """Clear cache before each test.""" |
| 306 | + django_cache.clear() |
| 307 | + |
| 308 | + def tearDown(self): |
| 309 | + """Clear cache after each test.""" |
| 310 | + django_cache.clear() |
| 311 | + |
| 312 | + def test_different_branch_different_cache_key(self): |
| 313 | + """Same data under different branches produces different cache keys.""" |
| 314 | + data = {"name": "Cisco"} |
| 315 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value="branch_a"): |
| 316 | + key_a = _find_obj_cache_key(data, "dcim.manufacturer") |
| 317 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value="branch_b"): |
| 318 | + key_b = _find_obj_cache_key(data, "dcim.manufacturer") |
| 319 | + self.assertNotEqual(key_a, key_b) |
| 320 | + |
| 321 | + def test_same_branch_same_cache_key(self): |
| 322 | + """Same data under the same branch produces identical cache keys.""" |
| 323 | + data = {"name": "Cisco"} |
| 324 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value="branch_a"): |
| 325 | + key1 = _find_obj_cache_key(data, "dcim.manufacturer") |
| 326 | + key2 = _find_obj_cache_key(data, "dcim.manufacturer") |
| 327 | + self.assertEqual(key1, key2) |
| 328 | + |
| 329 | + def test_no_branch_unchanged_from_legacy_format(self): |
| 330 | + """Without active branch, cache key matches the original format.""" |
| 331 | + data = {"name": "Cisco"} |
| 332 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value=None): |
| 333 | + key = _find_obj_cache_key(data, "dcim.manufacturer") |
| 334 | + self.assertTrue(key.startswith("diode:fobj:")) |
| 335 | + |
| 336 | + def test_branch_vs_no_branch_different_keys(self): |
| 337 | + """A branched key differs from a non-branched key for the same data.""" |
| 338 | + data = {"name": "Cisco"} |
| 339 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value=None): |
| 340 | + key_main = _find_obj_cache_key(data, "dcim.manufacturer") |
| 341 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value="branch_a"): |
| 342 | + key_branch = _find_obj_cache_key(data, "dcim.manufacturer") |
| 343 | + self.assertNotEqual(key_main, key_branch) |
| 344 | + |
| 345 | + def test_different_branch_different_rev_key(self): |
| 346 | + """Reverse-index keys are isolated per branch.""" |
| 347 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value="branch_a"): |
| 348 | + rev_a = _find_obj_rev_key("dcim.manufacturer", 100) |
| 349 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value="branch_b"): |
| 350 | + rev_b = _find_obj_rev_key("dcim.manufacturer", 100) |
| 351 | + self.assertNotEqual(rev_a, rev_b) |
| 352 | + self.assertIn("branch_a", rev_a) |
| 353 | + self.assertIn("branch_b", rev_b) |
| 354 | + |
| 355 | + def test_no_branch_rev_key_unchanged(self): |
| 356 | + """Without active branch, rev key matches the original format.""" |
| 357 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value=None): |
| 358 | + rev = _find_obj_rev_key("dcim.manufacturer", 100) |
| 359 | + self.assertEqual(rev, "diode:fobj:rev:dcim.manufacturer:100") |
| 360 | + |
| 361 | + |
| 362 | +class BranchAwareFindExistingObjectTestCase(TestCase): |
| 363 | + """Tests that find_existing_object cache does not cross branches.""" |
| 364 | + |
| 365 | + def setUp(self): |
| 366 | + """Set up test fixtures.""" |
| 367 | + self.manufacturer = Manufacturer.objects.create( |
| 368 | + name="BranchTestMfr", |
| 369 | + slug="branch-test-mfr", |
| 370 | + ) |
| 371 | + django_cache.clear() |
| 372 | + |
| 373 | + def tearDown(self): |
| 374 | + """Clean up cache after each test.""" |
| 375 | + django_cache.clear() |
| 376 | + |
| 377 | + @mock.patch( |
| 378 | + "netbox_diode_plugin.api.matcher._get_find_obj_cache_ttl", |
| 379 | + return_value=5, |
| 380 | + ) |
| 381 | + def test_cache_does_not_cross_branches(self, _mock_ttl): |
| 382 | + """Cache populated under branch A is a miss under branch B.""" |
| 383 | + data = {"name": "BranchTestMfr", "_object_type": "dcim.manufacturer"} |
| 384 | + |
| 385 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value="branch_a"): |
| 386 | + result_a = find_existing_object(data, "dcim.manufacturer") |
| 387 | + self.assertEqual(result_a.id, self.manufacturer.id) |
| 388 | + cache_key_a = _find_obj_cache_key(data, "dcim.manufacturer") |
| 389 | + self.assertIsNotNone(django_cache.get(cache_key_a)) |
| 390 | + |
| 391 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value="branch_b"): |
| 392 | + cache_key_b = _find_obj_cache_key(data, "dcim.manufacturer") |
| 393 | + self.assertIsNone(django_cache.get(cache_key_b)) |
| 394 | + |
| 395 | + @mock.patch( |
| 396 | + "netbox_diode_plugin.api.matcher._get_find_obj_cache_ttl", |
| 397 | + return_value=5, |
| 398 | + ) |
| 399 | + def test_invalidate_does_not_cross_branches(self, _mock_ttl): |
| 400 | + """Invalidation under branch B does not evict branch A's cache.""" |
| 401 | + data = {"name": "BranchTestMfr", "_object_type": "dcim.manufacturer"} |
| 402 | + |
| 403 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value="branch_a"): |
| 404 | + find_existing_object(data, "dcim.manufacturer") |
| 405 | + cache_key_a = _find_obj_cache_key(data, "dcim.manufacturer") |
| 406 | + self.assertIsNotNone(django_cache.get(cache_key_a)) |
| 407 | + |
| 408 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value="branch_b"): |
| 409 | + invalidate_find_obj_entry("dcim.manufacturer", self.manufacturer.id) |
| 410 | + |
| 411 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value="branch_a"): |
| 412 | + self.assertIsNotNone(django_cache.get(cache_key_a)) |
| 413 | + |
| 414 | + @mock.patch( |
| 415 | + "netbox_diode_plugin.api.matcher._get_find_obj_cache_ttl", |
| 416 | + return_value=5, |
| 417 | + ) |
| 418 | + def test_invalidate_within_same_branch(self, _mock_ttl): |
| 419 | + """Invalidation under the same branch correctly evicts cache.""" |
| 420 | + data = {"name": "BranchTestMfr", "_object_type": "dcim.manufacturer"} |
| 421 | + |
| 422 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value="branch_a"): |
| 423 | + find_existing_object(data, "dcim.manufacturer") |
| 424 | + cache_key_a = _find_obj_cache_key(data, "dcim.manufacturer") |
| 425 | + self.assertIsNotNone(django_cache.get(cache_key_a)) |
| 426 | + invalidate_find_obj_entry("dcim.manufacturer", self.manufacturer.id) |
| 427 | + self.assertIsNone(django_cache.get(cache_key_a)) |
| 428 | + |
| 429 | + @mock.patch( |
| 430 | + "netbox_diode_plugin.api.matcher._get_find_obj_cache_ttl", |
| 431 | + return_value=5, |
| 432 | + ) |
| 433 | + def test_branch_and_main_caches_independent(self, _mock_ttl): |
| 434 | + """Cache entries for main (no branch) and a branch are independent.""" |
| 435 | + data = {"name": "BranchTestMfr", "_object_type": "dcim.manufacturer"} |
| 436 | + |
| 437 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value=None): |
| 438 | + find_existing_object(data, "dcim.manufacturer") |
| 439 | + cache_key_main = _find_obj_cache_key(data, "dcim.manufacturer") |
| 440 | + |
| 441 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value="branch_a"): |
| 442 | + cache_key_branch = _find_obj_cache_key(data, "dcim.manufacturer") |
| 443 | + self.assertIsNone(django_cache.get(cache_key_branch)) |
| 444 | + |
| 445 | + with mock.patch(BRANCH_SCHEMA_MOCK, return_value=None): |
| 446 | + self.assertIsNotNone(django_cache.get(cache_key_main)) |
0 commit comments