using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Shared; using ZB.MOM.WW.CBDD.Tests; using Xunit; namespace ZB.MOM.WW.CBDD.Tests; /// /// Tests for circular references and N-N relationships /// Validates that the source generator handles: /// 1. Self-referencing entities using ObjectId references (Employee → ManagerId, DirectReportIds) /// 2. N-N via referencing with ObjectIds (CategoryRef/ProductRef) - BEST PRACTICE /// /// Note: Bidirectional embedding (Category ↔ Product with full objects) is NOT supported /// by the source generator and is an anti-pattern for document databases. /// Use referencing (ObjectIds) instead for N-N relationships. /// public class CircularReferenceTests : IDisposable { private readonly string _dbPath; private readonly Shared.TestDbContext _context; public CircularReferenceTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_circular_test_{Guid.NewGuid()}"); _context = new Shared.TestDbContext(_dbPath); } public void Dispose() { _context?.Dispose(); if (Directory.Exists(_dbPath)) { Directory.Delete(_dbPath, true); } } // ======================================== // Self-Reference Tests (Employee hierarchy with ObjectId references) // ======================================== [Fact] public void SelfReference_InsertAndQuery_ShouldWork() { // Arrange: Create organizational hierarchy using ObjectId references var ceoId = ObjectId.NewObjectId(); var manager1Id = ObjectId.NewObjectId(); var manager2Id = ObjectId.NewObjectId(); var developerId = ObjectId.NewObjectId(); var ceo = new Employee { Id = ceoId, Name = "Alice CEO", Department = "Executive", ManagerId = null, DirectReportIds = new List { manager1Id, manager2Id } }; var manager1 = new Employee { Id = manager1Id, Name = "Bob Manager", Department = "Engineering", ManagerId = ceoId, DirectReportIds = new List { developerId } }; var manager2 = new Employee { Id = manager2Id, Name = "Carol Manager", Department = "Sales", ManagerId = ceoId, DirectReportIds = new List() // No direct reports }; var developer = new Employee { Id = developerId, Name = "Dave Developer", Department = "Engineering", ManagerId = manager1Id, DirectReportIds = null // Leaf node }; // Act: Insert all employees _context.Employees.Insert(ceo); _context.Employees.Insert(manager1); _context.Employees.Insert(manager2); _context.Employees.Insert(developer); // Assert: Query and verify var queriedCeo = _context.Employees.FindById(ceoId); queriedCeo.ShouldNotBeNull(); queriedCeo.Name.ShouldBe("Alice CEO"); queriedCeo.DirectReportIds.ShouldNotBeNull(); queriedCeo.DirectReportIds.Count.ShouldBe(2); queriedCeo.DirectReportIds.ShouldContain(manager1Id); queriedCeo.DirectReportIds.ShouldContain(manager2Id); // Query manager and verify direct reports var queriedManager1 = _context.Employees.FindById(manager1Id); queriedManager1.ShouldNotBeNull(); queriedManager1.ManagerId.ShouldBe(ceoId); queriedManager1.DirectReportIds.ShouldNotBeNull(); queriedManager1.DirectReportIds.Count().ShouldBe(1); queriedManager1.DirectReportIds.ShouldContain(developerId); // Query developer and verify no direct reports var queriedDeveloper = _context.Employees.FindById(developerId); queriedDeveloper.ShouldNotBeNull(); queriedDeveloper.ManagerId.ShouldBe(manager1Id); // Empty list is acceptable (same as null semantically - no direct reports) (queriedDeveloper.DirectReportIds ?? new List()).ShouldBeEmpty(); } [Fact] public void SelfReference_UpdateDirectReports_ShouldPersist() { // Arrange: Create manager with one direct report var managerId = ObjectId.NewObjectId(); var employee1Id = ObjectId.NewObjectId(); var employee2Id = ObjectId.NewObjectId(); var manager = new Employee { Id = managerId, Name = "Manager", Department = "Engineering", DirectReportIds = new List { employee1Id } }; var employee1 = new Employee { Id = employee1Id, Name = "Employee 1", Department = "Engineering", ManagerId = managerId }; var employee2 = new Employee { Id = employee2Id, Name = "Employee 2", Department = "Engineering", ManagerId = managerId }; _context.Employees.Insert(manager); _context.Employees.Insert(employee1); _context.Employees.Insert(employee2); // Act: Add another direct report manager.DirectReportIds?.Add(employee2Id); _context.Employees.Update(manager); // Assert: Verify update persisted var queried = _context.Employees.FindById(managerId); queried.ShouldNotBeNull(); queried.DirectReportIds.ShouldNotBeNull(); queried.DirectReportIds.Count.ShouldBe(2); queried.DirectReportIds.ShouldContain(employee1Id); queried.DirectReportIds.ShouldContain(employee2Id); } [Fact] public void SelfReference_QueryByManagerId_ShouldWork() { // Arrange: Create hierarchy var managerId = ObjectId.NewObjectId(); var manager = new Employee { Id = managerId, Name = "Manager", Department = "Engineering" }; var employee1 = new Employee { Id = ObjectId.NewObjectId(), Name = "Employee 1", Department = "Engineering", ManagerId = managerId }; var employee2 = new Employee { Id = ObjectId.NewObjectId(), Name = "Employee 2", Department = "Engineering", ManagerId = managerId }; _context.Employees.Insert(manager); _context.Employees.Insert(employee1); _context.Employees.Insert(employee2); // Act: Query all employees with specific manager var subordinates = _context.Employees .AsQueryable() .Where(e => e.ManagerId == managerId) .ToList(); // Assert: Should find both employees subordinates.Count.ShouldBe(2); subordinates.ShouldContain(e => e.Name == "Employee 1"); subordinates.ShouldContain(e => e.Name == "Employee 2"); } // ======================================== // N-N Referencing Tests (CategoryRef/ProductRef) // BEST PRACTICE for document databases // ======================================== [Fact] public void NtoNReferencing_InsertAndQuery_ShouldWork() { // Arrange: Create categories and products with ObjectId references var categoryId1 = ObjectId.NewObjectId(); var categoryId2 = ObjectId.NewObjectId(); var productId1 = ObjectId.NewObjectId(); var productId2 = ObjectId.NewObjectId(); var electronics = new CategoryRef { Id = categoryId1, Name = "Electronics", Description = "Electronic devices", ProductIds = new List { productId1, productId2 } }; var computers = new CategoryRef { Id = categoryId2, Name = "Computers", Description = "Computing devices", ProductIds = new List { productId1 } }; var laptop = new ProductRef { Id = productId1, Name = "Laptop", Price = 999.99m, CategoryIds = new List { categoryId1, categoryId2 } }; var phone = new ProductRef { Id = productId2, Name = "Phone", Price = 599.99m, CategoryIds = new List { categoryId1 } }; // Act: Insert all entities _context.CategoryRefs.Insert(electronics); _context.CategoryRefs.Insert(computers); _context.ProductRefs.Insert(laptop); _context.ProductRefs.Insert(phone); // Assert: Query and verify references var queriedCategory = _context.CategoryRefs.FindById(categoryId1); queriedCategory.ShouldNotBeNull(); queriedCategory.Name.ShouldBe("Electronics"); queriedCategory.ProductIds.ShouldNotBeNull(); queriedCategory.ProductIds.Count.ShouldBe(2); queriedCategory.ProductIds.ShouldContain(productId1); queriedCategory.ProductIds.ShouldContain(productId2); var queriedProduct = _context.ProductRefs.FindById(productId1); queriedProduct.ShouldNotBeNull(); queriedProduct.Name.ShouldBe("Laptop"); queriedProduct.CategoryIds.ShouldNotBeNull(); queriedProduct.CategoryIds.Count.ShouldBe(2); queriedProduct.CategoryIds.ShouldContain(categoryId1); queriedProduct.CategoryIds.ShouldContain(categoryId2); } [Fact] public void NtoNReferencing_UpdateRelationships_ShouldPersist() { // Arrange: Create category and product var categoryId = ObjectId.NewObjectId(); var productId1 = ObjectId.NewObjectId(); var productId2 = ObjectId.NewObjectId(); var category = new CategoryRef { Id = categoryId, Name = "Books", Description = "Book category", ProductIds = new List { productId1 } }; var product1 = new ProductRef { Id = productId1, Name = "Book 1", Price = 19.99m, CategoryIds = new List { categoryId } }; var product2 = new ProductRef { Id = productId2, Name = "Book 2", Price = 29.99m, CategoryIds = new List() }; _context.CategoryRefs.Insert(category); _context.ProductRefs.Insert(product1); _context.ProductRefs.Insert(product2); // Act: Add product2 to category category.ProductIds?.Add(productId2); _context.CategoryRefs.Update(category); product2.CategoryIds?.Add(categoryId); _context.ProductRefs.Update(product2); // Assert: Verify relationships updated var queriedCategory = _context.CategoryRefs.FindById(categoryId); queriedCategory.ShouldNotBeNull(); queriedCategory.ProductIds.ShouldNotBeNull(); queriedCategory.ProductIds.Count.ShouldBe(2); queriedCategory.ProductIds.ShouldContain(productId2); var queriedProduct2 = _context.ProductRefs.FindById(productId2); queriedProduct2.ShouldNotBeNull(); queriedProduct2.CategoryIds.ShouldNotBeNull(); queriedProduct2.CategoryIds.Count().ShouldBe(1); queriedProduct2.CategoryIds.ShouldContain(categoryId); } [Fact] public void NtoNReferencing_DocumentSize_RemainSmall() { // Arrange: Create category referencing 100 products (only IDs) var categoryId = ObjectId.NewObjectId(); var productIds = Enumerable.Range(0, 100) .Select(_ => ObjectId.NewObjectId()) .ToList(); var category = new CategoryRef { Id = categoryId, Name = "Large Category", Description = "Category with 100 products", ProductIds = productIds }; // Act: Insert and query _context.CategoryRefs.Insert(category); var queried = _context.CategoryRefs.FindById(categoryId); // Assert: Document remains small (only ObjectIds, no embedding) queried.ShouldNotBeNull(); queried.ProductIds?.Count.ShouldBe(100); // Note: 100 ObjectIds = ~1.2KB (vs embedding full products = potentially hundreds of KBs) // This demonstrates why referencing is preferred for large N-N relationships } [Fact] public void NtoNReferencing_QueryByProductId_ShouldWork() { // Arrange: Create multiple categories referencing same product var productId = ObjectId.NewObjectId(); var category1 = new CategoryRef { Id = ObjectId.NewObjectId(), Name = "Category 1", Description = "First category", ProductIds = new List { productId } }; var category2 = new CategoryRef { Id = ObjectId.NewObjectId(), Name = "Category 2", Description = "Second category", ProductIds = new List { productId } }; _context.CategoryRefs.Insert(category1); _context.CategoryRefs.Insert(category2); // Act: Query all categories containing the product var categoriesWithProduct = _context.CategoryRefs .AsQueryable() .Where(c => c.ProductIds != null && c.ProductIds.Contains(productId)) .ToList(); // Assert: Should find both categories categoriesWithProduct.Count.ShouldBe(2); categoriesWithProduct.ShouldContain(c => c.Name == "Category 1"); categoriesWithProduct.ShouldContain(c => c.Name == "Category 2"); } }