Reformat / cleanup
This commit is contained in:
@@ -1,159 +1,187 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ZB.MOM.WW.CBDD.Bson;
|
||||
using ZB.MOM.WW.CBDD.Shared;
|
||||
using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Tests
|
||||
namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class AttributeTests
|
||||
{
|
||||
public class AttributeTests
|
||||
private readonly ConcurrentDictionary<string, ushort> _keyMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<ushort, string> _keys = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes lookup maps used by attribute mapper tests.
|
||||
/// </summary>
|
||||
public AttributeTests()
|
||||
{
|
||||
// Use full path for mapper until we are sure of the namespace
|
||||
private ZB_MOM_WW_CBDD_Shared_AnnotatedUserMapper CreateMapper() => new();
|
||||
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, ushort> _keyMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<ushort, string> _keys = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes lookup maps used by attribute mapper tests.
|
||||
/// </summary>
|
||||
public AttributeTests()
|
||||
ushort id = 1;
|
||||
string[] keys = ["_id", "display_name", "age", "location", "0", "1"];
|
||||
foreach (string key in keys)
|
||||
{
|
||||
ushort id = 1;
|
||||
string[] keys = ["_id", "display_name", "age", "location", "0", "1"];
|
||||
foreach (var key in keys)
|
||||
{
|
||||
_keyMap[key] = id;
|
||||
_keys[id] = key;
|
||||
id++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies table attribute mapping resolves the expected collection name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_Table_Attribute_Mapping()
|
||||
{
|
||||
// Verify that the generated mapper has the correct collection name
|
||||
var mapper = CreateMapper();
|
||||
mapper.CollectionName.ShouldBe("test.custom_users");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies required attribute validation is enforced.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_Required_Validation()
|
||||
{
|
||||
var mapper = CreateMapper();
|
||||
var user = new AnnotatedUser { Name = "" }; // Required name is empty
|
||||
var writer = new BsonSpanWriter(new byte[1024], _keyMap);
|
||||
|
||||
bool thrown = false;
|
||||
try
|
||||
{
|
||||
mapper.Serialize(user, writer);
|
||||
}
|
||||
catch (ValidationException)
|
||||
{
|
||||
thrown = true;
|
||||
}
|
||||
thrown.ShouldBeTrue("Should throw ValidationException for empty Name.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies string length attribute validation is enforced.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_StringLength_Validation()
|
||||
{
|
||||
var mapper = CreateMapper();
|
||||
var user = new AnnotatedUser { Name = "Jo" }; // Too short
|
||||
var writer = new BsonSpanWriter(new byte[1024], _keyMap);
|
||||
|
||||
bool thrown = false;
|
||||
try { mapper.Serialize(user, writer); } catch (ValidationException) { thrown = true; }
|
||||
thrown.ShouldBeTrue("Should throw ValidationException for Name too short.");
|
||||
|
||||
user.Name = new string('A', 51); // Too long
|
||||
thrown = false;
|
||||
try { mapper.Serialize(user, writer); } catch (ValidationException) { thrown = true; }
|
||||
thrown.ShouldBeTrue("Should throw ValidationException for Name too long.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies range attribute validation is enforced.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_Range_Validation()
|
||||
{
|
||||
var mapper = CreateMapper();
|
||||
var user = new AnnotatedUser { Name = "John", Age = 200 }; // Out of range
|
||||
var writer = new BsonSpanWriter(new byte[1024], _keyMap);
|
||||
|
||||
bool thrown = false;
|
||||
try { mapper.Serialize(user, writer); } catch (ValidationException) { thrown = true; }
|
||||
thrown.ShouldBeTrue("Should throw ValidationException for Age out of range.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies column attribute maps to the expected BSON field name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_Column_Name_Mapping()
|
||||
{
|
||||
var mapper = CreateMapper();
|
||||
var user = new AnnotatedUser { Name = "John", Age = 30 };
|
||||
var buffer = new byte[1024];
|
||||
var writer = new BsonSpanWriter(buffer, _keyMap);
|
||||
|
||||
mapper.Serialize(user, writer);
|
||||
|
||||
var reader = new BsonSpanReader(buffer, _keys);
|
||||
reader.ReadDocumentSize();
|
||||
|
||||
bool foundDisplayName = false;
|
||||
while (reader.Remaining > 0)
|
||||
{
|
||||
var type = reader.ReadBsonType();
|
||||
if (type == BsonType.EndOfDocument) break;
|
||||
|
||||
var name = reader.ReadElementHeader();
|
||||
if (name == "display_name") foundDisplayName = true;
|
||||
reader.SkipValue(type);
|
||||
}
|
||||
|
||||
foundDisplayName.ShouldBeTrue("BSON field name should be 'display_name' from [Column] attribute.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies not-mapped attribute excludes properties from BSON serialization.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_NotMapped_Attribute()
|
||||
{
|
||||
var mapper = CreateMapper();
|
||||
var user = new AnnotatedUser { Name = "John", Age = 30 };
|
||||
var buffer = new byte[1024];
|
||||
var writer = new BsonSpanWriter(buffer, _keyMap);
|
||||
|
||||
mapper.Serialize(user, writer);
|
||||
|
||||
var reader = new BsonSpanReader(buffer, _keys);
|
||||
reader.ReadDocumentSize();
|
||||
|
||||
bool foundComputed = false;
|
||||
while (reader.Remaining > 0)
|
||||
{
|
||||
var type = reader.ReadBsonType();
|
||||
if (type == BsonType.EndOfDocument) break;
|
||||
|
||||
var name = reader.ReadElementHeader();
|
||||
if (name == "ComputedInfo") foundComputed = true;
|
||||
reader.SkipValue(type);
|
||||
}
|
||||
|
||||
foundComputed.ShouldBeFalse("ComputedInfo should not be mapped to BSON.");
|
||||
_keyMap[key] = id;
|
||||
_keys[id] = key;
|
||||
id++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use full path for mapper until we are sure of the namespace
|
||||
private ZB_MOM_WW_CBDD_Shared_AnnotatedUserMapper CreateMapper()
|
||||
{
|
||||
return new ZB_MOM_WW_CBDD_Shared_AnnotatedUserMapper();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies table attribute mapping resolves the expected collection name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_Table_Attribute_Mapping()
|
||||
{
|
||||
// Verify that the generated mapper has the correct collection name
|
||||
var mapper = CreateMapper();
|
||||
mapper.CollectionName.ShouldBe("test.custom_users");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies required attribute validation is enforced.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_Required_Validation()
|
||||
{
|
||||
var mapper = CreateMapper();
|
||||
var user = new AnnotatedUser { Name = "" }; // Required name is empty
|
||||
var writer = new BsonSpanWriter(new byte[1024], _keyMap);
|
||||
|
||||
var thrown = false;
|
||||
try
|
||||
{
|
||||
mapper.Serialize(user, writer);
|
||||
}
|
||||
catch (ValidationException)
|
||||
{
|
||||
thrown = true;
|
||||
}
|
||||
|
||||
thrown.ShouldBeTrue("Should throw ValidationException for empty Name.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies string length attribute validation is enforced.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_StringLength_Validation()
|
||||
{
|
||||
var mapper = CreateMapper();
|
||||
var user = new AnnotatedUser { Name = "Jo" }; // Too short
|
||||
var writer = new BsonSpanWriter(new byte[1024], _keyMap);
|
||||
|
||||
var thrown = false;
|
||||
try
|
||||
{
|
||||
mapper.Serialize(user, writer);
|
||||
}
|
||||
catch (ValidationException)
|
||||
{
|
||||
thrown = true;
|
||||
}
|
||||
|
||||
thrown.ShouldBeTrue("Should throw ValidationException for Name too short.");
|
||||
|
||||
user.Name = new string('A', 51); // Too long
|
||||
thrown = false;
|
||||
try
|
||||
{
|
||||
mapper.Serialize(user, writer);
|
||||
}
|
||||
catch (ValidationException)
|
||||
{
|
||||
thrown = true;
|
||||
}
|
||||
|
||||
thrown.ShouldBeTrue("Should throw ValidationException for Name too long.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies range attribute validation is enforced.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_Range_Validation()
|
||||
{
|
||||
var mapper = CreateMapper();
|
||||
var user = new AnnotatedUser { Name = "John", Age = 200 }; // Out of range
|
||||
var writer = new BsonSpanWriter(new byte[1024], _keyMap);
|
||||
|
||||
var thrown = false;
|
||||
try
|
||||
{
|
||||
mapper.Serialize(user, writer);
|
||||
}
|
||||
catch (ValidationException)
|
||||
{
|
||||
thrown = true;
|
||||
}
|
||||
|
||||
thrown.ShouldBeTrue("Should throw ValidationException for Age out of range.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies column attribute maps to the expected BSON field name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_Column_Name_Mapping()
|
||||
{
|
||||
var mapper = CreateMapper();
|
||||
var user = new AnnotatedUser { Name = "John", Age = 30 };
|
||||
var buffer = new byte[1024];
|
||||
var writer = new BsonSpanWriter(buffer, _keyMap);
|
||||
|
||||
mapper.Serialize(user, writer);
|
||||
|
||||
var reader = new BsonSpanReader(buffer, _keys);
|
||||
reader.ReadDocumentSize();
|
||||
|
||||
var foundDisplayName = false;
|
||||
while (reader.Remaining > 0)
|
||||
{
|
||||
var type = reader.ReadBsonType();
|
||||
if (type == BsonType.EndOfDocument) break;
|
||||
|
||||
string name = reader.ReadElementHeader();
|
||||
if (name == "display_name") foundDisplayName = true;
|
||||
reader.SkipValue(type);
|
||||
}
|
||||
|
||||
foundDisplayName.ShouldBeTrue("BSON field name should be 'display_name' from [Column] attribute.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies not-mapped attribute excludes properties from BSON serialization.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_NotMapped_Attribute()
|
||||
{
|
||||
var mapper = CreateMapper();
|
||||
var user = new AnnotatedUser { Name = "John", Age = 30 };
|
||||
var buffer = new byte[1024];
|
||||
var writer = new BsonSpanWriter(buffer, _keyMap);
|
||||
|
||||
mapper.Serialize(user, writer);
|
||||
|
||||
var reader = new BsonSpanReader(buffer, _keys);
|
||||
reader.ReadDocumentSize();
|
||||
|
||||
var foundComputed = false;
|
||||
while (reader.Remaining > 0)
|
||||
{
|
||||
var type = reader.ReadBsonType();
|
||||
if (type == BsonType.EndOfDocument) break;
|
||||
|
||||
string name = reader.ReadElementHeader();
|
||||
if (name == "ComputedInfo") foundComputed = true;
|
||||
reader.SkipValue(type);
|
||||
}
|
||||
|
||||
foundComputed.ShouldBeFalse("ComputedInfo should not be mapped to BSON.");
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,38 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class CircularReferenceTests : IDisposable
|
||||
{
|
||||
private readonly TestDbContext _context;
|
||||
private readonly string _dbPath;
|
||||
private readonly Shared.TestDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CircularReferenceTests"/> class.
|
||||
/// Initializes a new instance of the <see cref="CircularReferenceTests" /> class.
|
||||
/// </summary>
|
||||
public CircularReferenceTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_circular_test_{Guid.NewGuid()}");
|
||||
_context = new Shared.TestDbContext(_dbPath);
|
||||
_context = new TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Dispose.
|
||||
/// Executes Dispose.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_context?.Dispose();
|
||||
if (Directory.Exists(_dbPath))
|
||||
{
|
||||
Directory.Delete(_dbPath, true);
|
||||
}
|
||||
if (Directory.Exists(_dbPath)) Directory.Delete(_dbPath, true);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
@@ -46,7 +40,7 @@ public class CircularReferenceTests : IDisposable
|
||||
// ========================================
|
||||
|
||||
/// <summary>
|
||||
/// Executes SelfReference_InsertAndQuery_ShouldWork.
|
||||
/// Executes SelfReference_InsertAndQuery_ShouldWork.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SelfReference_InsertAndQuery_ShouldWork()
|
||||
@@ -125,7 +119,7 @@ public class CircularReferenceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes SelfReference_UpdateDirectReports_ShouldPersist.
|
||||
/// Executes SelfReference_UpdateDirectReports_ShouldPersist.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SelfReference_UpdateDirectReports_ShouldPersist()
|
||||
@@ -177,7 +171,7 @@ public class CircularReferenceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes SelfReference_QueryByManagerId_ShouldWork.
|
||||
/// Executes SelfReference_QueryByManagerId_ShouldWork.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SelfReference_QueryByManagerId_ShouldWork()
|
||||
@@ -230,7 +224,7 @@ public class CircularReferenceTests : IDisposable
|
||||
// ========================================
|
||||
|
||||
/// <summary>
|
||||
/// Executes NtoNReferencing_InsertAndQuery_ShouldWork.
|
||||
/// Executes NtoNReferencing_InsertAndQuery_ShouldWork.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NtoNReferencing_InsertAndQuery_ShouldWork()
|
||||
@@ -298,7 +292,7 @@ public class CircularReferenceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes NtoNReferencing_UpdateRelationships_ShouldPersist.
|
||||
/// Executes NtoNReferencing_UpdateRelationships_ShouldPersist.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NtoNReferencing_UpdateRelationships_ShouldPersist()
|
||||
@@ -358,7 +352,7 @@ public class CircularReferenceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes NtoNReferencing_DocumentSize_RemainSmall.
|
||||
/// Executes NtoNReferencing_DocumentSize_RemainSmall.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NtoNReferencing_DocumentSize_RemainSmall()
|
||||
@@ -390,7 +384,7 @@ public class CircularReferenceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes NtoNReferencing_QueryByProductId_ShouldWork.
|
||||
/// Executes NtoNReferencing_QueryByProductId_ShouldWork.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NtoNReferencing_QueryByProductId_ShouldWork()
|
||||
@@ -428,4 +422,4 @@ public class CircularReferenceTests : IDisposable
|
||||
categoriesWithProduct.ShouldContain(c => c.Name == "Category 1");
|
||||
categoriesWithProduct.ShouldContain(c => c.Name == "Category 2");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,170 +1,169 @@
|
||||
using ZB.MOM.WW.CBDD.Shared;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Tests
|
||||
namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for entities with nullable string Id (like UuidEntity scenario from CleanCore)
|
||||
/// This reproduces the bug where the generator incorrectly chose ObjectIdMapperBase
|
||||
/// instead of StringMapperBase for inherited nullable string Id properties
|
||||
/// </summary>
|
||||
public class NullableStringIdTests : IDisposable
|
||||
{
|
||||
private const string DbPath = "nullable_string_id.db";
|
||||
|
||||
/// <summary>
|
||||
/// Tests for entities with nullable string Id (like UuidEntity scenario from CleanCore)
|
||||
/// This reproduces the bug where the generator incorrectly chose ObjectIdMapperBase
|
||||
/// instead of StringMapperBase for inherited nullable string Id properties
|
||||
/// Initializes a new instance of the <see cref="NullableStringIdTests" /> class.
|
||||
/// </summary>
|
||||
public class NullableStringIdTests : System.IDisposable
|
||||
public NullableStringIdTests()
|
||||
{
|
||||
private const string DbPath = "nullable_string_id.db";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NullableStringIdTests"/> class.
|
||||
/// </summary>
|
||||
public NullableStringIdTests()
|
||||
{
|
||||
if (File.Exists(DbPath)) File.Delete(DbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (File.Exists(DbPath)) File.Delete(DbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the mock counter collection is initialized.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Collection_IsInitialized()
|
||||
{
|
||||
using var db = new Shared.TestDbContext(DbPath);
|
||||
|
||||
// Verify Collection is not null (initialized by generated method)
|
||||
db.MockCounters.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies insert and find-by-id operations work for string identifiers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Insert_And_FindById_Works()
|
||||
{
|
||||
using var db = new Shared.TestDbContext(DbPath);
|
||||
|
||||
var counter = new MockCounter("test-id-123")
|
||||
{
|
||||
Name = "TestCounter",
|
||||
Value = 42
|
||||
};
|
||||
|
||||
// Insert should work with string Id
|
||||
db.MockCounters.Insert(counter);
|
||||
|
||||
// FindById should retrieve the entity
|
||||
var stored = db.MockCounters.FindById("test-id-123");
|
||||
stored.ShouldNotBeNull();
|
||||
stored.Id.ShouldBe("test-id-123");
|
||||
stored.Name.ShouldBe("TestCounter");
|
||||
stored.Value.ShouldBe(42);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies update operations work for string identifiers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Update_Works()
|
||||
{
|
||||
using var db = new Shared.TestDbContext(DbPath);
|
||||
|
||||
var counter = new MockCounter("update-test")
|
||||
{
|
||||
Name = "Original",
|
||||
Value = 10
|
||||
};
|
||||
|
||||
db.MockCounters.Insert(counter);
|
||||
|
||||
// Update the entity
|
||||
counter.Name = "Updated";
|
||||
counter.Value = 20;
|
||||
db.MockCounters.Update(counter);
|
||||
|
||||
// Verify update
|
||||
var updated = db.MockCounters.FindById("update-test");
|
||||
updated.ShouldNotBeNull();
|
||||
updated.Name.ShouldBe("Updated");
|
||||
updated.Value.ShouldBe(20);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies delete operations work for string identifiers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Delete_Works()
|
||||
{
|
||||
using var db = new Shared.TestDbContext(DbPath);
|
||||
|
||||
var counter = new MockCounter("delete-test")
|
||||
{
|
||||
Name = "ToDelete",
|
||||
Value = 99
|
||||
};
|
||||
|
||||
db.MockCounters.Insert(counter);
|
||||
db.MockCounters.FindById("delete-test").ShouldNotBeNull();
|
||||
|
||||
// Delete the entity
|
||||
db.MockCounters.Delete("delete-test");
|
||||
|
||||
// Verify deletion
|
||||
var deleted = db.MockCounters.FindById("delete-test");
|
||||
deleted.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies query operations work for string identifiers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Query_Works()
|
||||
{
|
||||
using var db = new Shared.TestDbContext(DbPath);
|
||||
|
||||
db.MockCounters.Insert(new MockCounter("q1") { Name = "First", Value = 100 });
|
||||
db.MockCounters.Insert(new MockCounter("q2") { Name = "Second", Value = 200 });
|
||||
db.MockCounters.Insert(new MockCounter("q3") { Name = "Third", Value = 150 });
|
||||
|
||||
// Query all
|
||||
var all = db.MockCounters.AsQueryable().ToList();
|
||||
all.Count.ShouldBe(3);
|
||||
|
||||
// Query with condition
|
||||
var highValues = db.MockCounters.AsQueryable()
|
||||
.Where(c => c.Value > 150)
|
||||
.ToList();
|
||||
|
||||
highValues.Count().ShouldBe(1);
|
||||
highValues[0].Name.ShouldBe("Second");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies inherited string identifiers are stored and retrieved correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_InheritedId_IsStoredCorrectly()
|
||||
{
|
||||
using var db = new Shared.TestDbContext(DbPath);
|
||||
|
||||
// Test that the inherited nullable string Id from MockBaseEntity works correctly
|
||||
var counter = new MockCounter("inherited-id-test")
|
||||
{
|
||||
Name = "Inherited",
|
||||
Value = 777
|
||||
};
|
||||
|
||||
db.MockCounters.Insert(counter);
|
||||
|
||||
var stored = db.MockCounters.FindById("inherited-id-test");
|
||||
stored.ShouldNotBeNull();
|
||||
|
||||
// Verify the Id is correctly stored and retrieved through inheritance
|
||||
stored.Id.ShouldBe("inherited-id-test");
|
||||
stored.Id.ShouldBeOfType<string>();
|
||||
}
|
||||
if (File.Exists(DbPath)) File.Delete(DbPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (File.Exists(DbPath)) File.Delete(DbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the mock counter collection is initialized.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Collection_IsInitialized()
|
||||
{
|
||||
using var db = new TestDbContext(DbPath);
|
||||
|
||||
// Verify Collection is not null (initialized by generated method)
|
||||
db.MockCounters.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies insert and find-by-id operations work for string identifiers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Insert_And_FindById_Works()
|
||||
{
|
||||
using var db = new TestDbContext(DbPath);
|
||||
|
||||
var counter = new MockCounter("test-id-123")
|
||||
{
|
||||
Name = "TestCounter",
|
||||
Value = 42
|
||||
};
|
||||
|
||||
// Insert should work with string Id
|
||||
db.MockCounters.Insert(counter);
|
||||
|
||||
// FindById should retrieve the entity
|
||||
var stored = db.MockCounters.FindById("test-id-123");
|
||||
stored.ShouldNotBeNull();
|
||||
stored.Id.ShouldBe("test-id-123");
|
||||
stored.Name.ShouldBe("TestCounter");
|
||||
stored.Value.ShouldBe(42);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies update operations work for string identifiers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Update_Works()
|
||||
{
|
||||
using var db = new TestDbContext(DbPath);
|
||||
|
||||
var counter = new MockCounter("update-test")
|
||||
{
|
||||
Name = "Original",
|
||||
Value = 10
|
||||
};
|
||||
|
||||
db.MockCounters.Insert(counter);
|
||||
|
||||
// Update the entity
|
||||
counter.Name = "Updated";
|
||||
counter.Value = 20;
|
||||
db.MockCounters.Update(counter);
|
||||
|
||||
// Verify update
|
||||
var updated = db.MockCounters.FindById("update-test");
|
||||
updated.ShouldNotBeNull();
|
||||
updated.Name.ShouldBe("Updated");
|
||||
updated.Value.ShouldBe(20);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies delete operations work for string identifiers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Delete_Works()
|
||||
{
|
||||
using var db = new TestDbContext(DbPath);
|
||||
|
||||
var counter = new MockCounter("delete-test")
|
||||
{
|
||||
Name = "ToDelete",
|
||||
Value = 99
|
||||
};
|
||||
|
||||
db.MockCounters.Insert(counter);
|
||||
db.MockCounters.FindById("delete-test").ShouldNotBeNull();
|
||||
|
||||
// Delete the entity
|
||||
db.MockCounters.Delete("delete-test");
|
||||
|
||||
// Verify deletion
|
||||
var deleted = db.MockCounters.FindById("delete-test");
|
||||
deleted.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies query operations work for string identifiers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Query_Works()
|
||||
{
|
||||
using var db = new TestDbContext(DbPath);
|
||||
|
||||
db.MockCounters.Insert(new MockCounter("q1") { Name = "First", Value = 100 });
|
||||
db.MockCounters.Insert(new MockCounter("q2") { Name = "Second", Value = 200 });
|
||||
db.MockCounters.Insert(new MockCounter("q3") { Name = "Third", Value = 150 });
|
||||
|
||||
// Query all
|
||||
var all = db.MockCounters.AsQueryable().ToList();
|
||||
all.Count.ShouldBe(3);
|
||||
|
||||
// Query with condition
|
||||
var highValues = db.MockCounters.AsQueryable()
|
||||
.Where(c => c.Value > 150)
|
||||
.ToList();
|
||||
|
||||
highValues.Count().ShouldBe(1);
|
||||
highValues[0].Name.ShouldBe("Second");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies inherited string identifiers are stored and retrieved correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_InheritedId_IsStoredCorrectly()
|
||||
{
|
||||
using var db = new TestDbContext(DbPath);
|
||||
|
||||
// Test that the inherited nullable string Id from MockBaseEntity works correctly
|
||||
var counter = new MockCounter("inherited-id-test")
|
||||
{
|
||||
Name = "Inherited",
|
||||
Value = 777
|
||||
};
|
||||
|
||||
db.MockCounters.Insert(counter);
|
||||
|
||||
var stored = db.MockCounters.FindById("inherited-id-test");
|
||||
stored.ShouldNotBeNull();
|
||||
|
||||
// Verify the Id is correctly stored and retrieved through inheritance
|
||||
stored.Id.ShouldBe("inherited-id-test");
|
||||
stored.Id.ShouldBeOfType<string>();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,29 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
using System.Collections.Concurrent;
|
||||
using ZB.MOM.WW.CBDD.Bson;
|
||||
using ZB.MOM.WW.CBDD.Bson.Schema;
|
||||
using ZB.MOM.WW.CBDD.Core.Storage;
|
||||
using ZB.MOM.WW.CBDD.Core.Collections;
|
||||
using ZB.MOM.WW.CBDD.Core.Indexing;
|
||||
using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers;
|
||||
using ZB.MOM.WW.CBDD.Core.Storage;
|
||||
using ZB.MOM.WW.CBDD.Shared;
|
||||
using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class SchemaPersistenceTests : IDisposable
|
||||
{
|
||||
private readonly TestDbContext _db;
|
||||
private readonly string _dbPath;
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SchemaPersistenceTests"/> class.
|
||||
/// Initializes a new instance of the <see cref="SchemaPersistenceTests" /> class.
|
||||
/// </summary>
|
||||
public SchemaPersistenceTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"schema_test_{Guid.NewGuid()}.db");
|
||||
_db = new Shared.TestDbContext(_dbPath);
|
||||
_db = new TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources and removes temporary files.
|
||||
/// Disposes test resources and removes temporary files.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
@@ -36,7 +32,7 @@ public class SchemaPersistenceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies BSON schema serialization and deserialization round-trips correctly.
|
||||
/// Verifies BSON schema serialization and deserialization round-trips correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BsonSchema_Serialization_RoundTrip()
|
||||
@@ -65,12 +61,16 @@ public class SchemaPersistenceTests : IDisposable
|
||||
};
|
||||
|
||||
var buffer = new byte[1024];
|
||||
var keyMap = new System.Collections.Concurrent.ConcurrentDictionary<string, ushort>(StringComparer.OrdinalIgnoreCase);
|
||||
var keys = new System.Collections.Concurrent.ConcurrentDictionary<ushort, string>();
|
||||
var keyMap = new ConcurrentDictionary<string, ushort>(StringComparer.OrdinalIgnoreCase);
|
||||
var keys = new ConcurrentDictionary<ushort, string>();
|
||||
|
||||
// Manual registration for schema keys
|
||||
ushort id = 1;
|
||||
foreach (var k in new[] { "person", "id", "name", "age", "address", "city", "fields", "title", "type", "isnullable", "nestedschema", "t", "v", "f", "n", "b", "s", "a", "_v", "0", "1", "2", "3", "4", "5" })
|
||||
foreach (string k in new[]
|
||||
{
|
||||
"person", "id", "name", "age", "address", "city", "fields", "title", "type", "isnullable",
|
||||
"nestedschema", "t", "v", "f", "n", "b", "s", "a", "_v", "0", "1", "2", "3", "4", "5"
|
||||
})
|
||||
{
|
||||
keyMap[k] = id;
|
||||
keys[id] = k;
|
||||
@@ -91,7 +91,7 @@ public class SchemaPersistenceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies collection metadata is persisted and reloaded correctly.
|
||||
/// Verifies collection metadata is persisted and reloaded correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StorageEngine_Collections_Metadata_Persistence()
|
||||
@@ -102,7 +102,8 @@ public class SchemaPersistenceTests : IDisposable
|
||||
PrimaryRootPageId = 10,
|
||||
SchemaRootPageId = 20
|
||||
};
|
||||
meta.Indexes.Add(new IndexMetadata { Name = "age", IsUnique = false, Type = IndexType.BTree, PropertyPaths = ["Age"] });
|
||||
meta.Indexes.Add(new IndexMetadata
|
||||
{ Name = "age", IsUnique = false, Type = IndexType.BTree, PropertyPaths = ["Age"] });
|
||||
|
||||
_db.Storage.SaveCollectionMetadata(meta);
|
||||
|
||||
@@ -116,38 +117,48 @@ public class SchemaPersistenceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies schema versioning appends new schema versions correctly.
|
||||
/// Verifies schema versioning appends new schema versions correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StorageEngine_Schema_Versioning()
|
||||
{
|
||||
var schema1 = new BsonSchema { Title = "V1", Fields = { new BsonField { Name = "f1", Type = BsonType.String } } };
|
||||
var schema2 = new BsonSchema { Title = "V2", Fields = { new BsonField { Name = "f1", Type = BsonType.String }, new BsonField { Name = "f2", Type = BsonType.Int32 } } };
|
||||
var schema1 = new BsonSchema
|
||||
{ Title = "V1", Fields = { new BsonField { Name = "f1", Type = BsonType.String } } };
|
||||
var schema2 = new BsonSchema
|
||||
{
|
||||
Title = "V2",
|
||||
Fields =
|
||||
{
|
||||
new BsonField { Name = "f1", Type = BsonType.String },
|
||||
new BsonField { Name = "f2", Type = BsonType.Int32 }
|
||||
}
|
||||
};
|
||||
|
||||
var rootId = _db.Storage.AppendSchema(0, schema1);
|
||||
uint rootId = _db.Storage.AppendSchema(0, schema1);
|
||||
rootId.ShouldNotBe(0u);
|
||||
|
||||
var schemas = _db.Storage.GetSchemas(rootId);
|
||||
schemas.Count().ShouldBe(1);
|
||||
schemas[0].Title.ShouldBe("V1");
|
||||
|
||||
var updatedRoot = _db.Storage.AppendSchema(rootId, schema2);
|
||||
uint updatedRoot = _db.Storage.AppendSchema(rootId, schema2);
|
||||
updatedRoot.ShouldBe(rootId);
|
||||
|
||||
schemas = _db.Storage.GetSchemas(rootId);
|
||||
schemas.Count.ShouldBe(2, $"Expected 2 schemas but found {schemas.Count}. Titles: {(schemas.Count > 0 ? string.Join(", ", schemas.Select(s => s.Title)) : "None")}");
|
||||
schemas.Count.ShouldBe(2,
|
||||
$"Expected 2 schemas but found {schemas.Count}. Titles: {(schemas.Count > 0 ? string.Join(", ", schemas.Select(s => s.Title)) : "None")}");
|
||||
schemas[0].Title.ShouldBe("V1");
|
||||
schemas[1].Title.ShouldBe("V2");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies collection startup integrates schema versioning behavior.
|
||||
/// Verifies collection startup integrates schema versioning behavior.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DocumentCollection_Integrates_Schema_Versioning_On_Startup()
|
||||
{
|
||||
// Use a dedicated database for this test to avoid schema pollution from _db
|
||||
var testDbPath = Path.Combine(Path.GetTempPath(), $"schema_versioning_test_{Guid.NewGuid()}.db");
|
||||
string testDbPath = Path.Combine(Path.GetTempPath(), $"schema_versioning_test_{Guid.NewGuid()}.db");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -155,7 +166,7 @@ public class SchemaPersistenceTests : IDisposable
|
||||
var schema1 = mapper1.GetSchema();
|
||||
|
||||
// 1. First startup - create DB and initialize Person collection
|
||||
using (var db1 = new Shared.TestDbContext(testDbPath))
|
||||
using (var db1 = new TestDbContext(testDbPath))
|
||||
{
|
||||
// Access only People collection to avoid initializing others
|
||||
var coll = db1.People;
|
||||
@@ -171,7 +182,7 @@ public class SchemaPersistenceTests : IDisposable
|
||||
}
|
||||
|
||||
// 2. Restart with SAME schema (should NOT append)
|
||||
using (var db2 = new Shared.TestDbContext(testDbPath))
|
||||
using (var db2 = new TestDbContext(testDbPath))
|
||||
{
|
||||
var coll = db2.People;
|
||||
var meta = db2.Storage.GetCollectionMetadata("people_collection");
|
||||
@@ -186,7 +197,7 @@ public class SchemaPersistenceTests : IDisposable
|
||||
// Since we can't change the actual Person class at runtime, this test verifies
|
||||
// that the same schema doesn't get re-appended.
|
||||
// A real-world scenario would involve deploying a new mapper version.
|
||||
using (var db3 = new Shared.TestDbContext(testDbPath))
|
||||
using (var db3 = new TestDbContext(testDbPath))
|
||||
{
|
||||
var coll = db3.People;
|
||||
var meta = db3.Storage.GetCollectionMetadata("people_collection");
|
||||
@@ -205,7 +216,7 @@ public class SchemaPersistenceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies persisted documents include the schema version field.
|
||||
/// Verifies persisted documents include the schema version field.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Document_Contains_Schema_Version_Field()
|
||||
@@ -214,7 +225,7 @@ public class SchemaPersistenceTests : IDisposable
|
||||
using (var coll = _db.People)
|
||||
{
|
||||
var person = new Person { Name = "John" };
|
||||
var id = coll.Insert(person);
|
||||
int id = coll.Insert(person);
|
||||
_db.SaveChanges();
|
||||
|
||||
coll.Count().ShouldBe(1);
|
||||
@@ -232,7 +243,7 @@ public class SchemaPersistenceTests : IDisposable
|
||||
// Read raw bytes from page
|
||||
var pageBuffer = new byte[_db.Storage.PageSize];
|
||||
_db.Storage.ReadPage(location.PageId, 0, pageBuffer);
|
||||
var slotOffset = SlottedPageHeader.Size + (location.SlotIndex * SlotEntry.Size);
|
||||
int slotOffset = SlottedPageHeader.Size + location.SlotIndex * SlotEntry.Size;
|
||||
var slot = SlotEntry.ReadFrom(pageBuffer.AsSpan(slotOffset));
|
||||
var docData = pageBuffer.AsSpan(slot.Offset, slot.Length);
|
||||
|
||||
@@ -241,7 +252,7 @@ public class SchemaPersistenceTests : IDisposable
|
||||
|
||||
// Look for _v (BsonType.Int32 + 2-byte ID)
|
||||
ushort vId = _db.Storage.GetKeyMap()["_v"];
|
||||
string vIdHex = vId.ToString("X4");
|
||||
var vIdHex = vId.ToString("X4");
|
||||
// Reverse endian for hex string check (ushort is LE)
|
||||
string vIdHexLE = vIdHex.Substring(2, 2) + vIdHex.Substring(0, 2);
|
||||
string pattern = "10" + vIdHexLE;
|
||||
@@ -255,4 +266,4 @@ public class SchemaPersistenceTests : IDisposable
|
||||
valueHex.ShouldBe("01000000");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
using System.Collections.Concurrent;
|
||||
using ZB.MOM.WW.CBDD.Bson;
|
||||
using ZB.MOM.WW.CBDD.Core.Collections;
|
||||
using ZB.MOM.WW.CBDD.Core.Indexing;
|
||||
using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class SchemaTests
|
||||
{
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, ushort> _testKeyMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly ConcurrentDictionary<string, ushort> _testKeyMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static SchemaTests()
|
||||
{
|
||||
ushort id = 1;
|
||||
foreach (var k in new[] { "_id", "name", "mainaddress", "otheraddresses", "tags", "secret", "street", "city" }) _testKeyMap[k] = id++;
|
||||
foreach (string k in new[]
|
||||
{ "_id", "name", "mainaddress", "otheraddresses", "tags", "secret", "street", "city" })
|
||||
_testKeyMap[k] = id++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes UsedKeys_ShouldReturnAllKeys.
|
||||
/// Executes UsedKeys_ShouldReturnAllKeys.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void UsedKeys_ShouldReturnAllKeys()
|
||||
@@ -33,11 +33,10 @@ public class SchemaTests
|
||||
keys.ShouldContain("secret");
|
||||
keys.ShouldContain("street");
|
||||
keys.ShouldContain("city");
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes GetSchema_ShouldReturnBsonSchema.
|
||||
/// Executes GetSchema_ShouldReturnBsonSchema.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetSchema_ShouldReturnBsonSchema()
|
||||
@@ -60,4 +59,4 @@ public class SchemaTests
|
||||
// Address in MockEntities has City (Nested)
|
||||
addressField.NestedSchema.Fields.ShouldContain(f => f.Name == "city");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,253 +1,250 @@
|
||||
using System;
|
||||
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
|
||||
namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class TemporalTypesTests : IDisposable
|
||||
{
|
||||
public class TemporalTypesTests : IDisposable
|
||||
private readonly TestDbContext _db;
|
||||
private readonly string _dbPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TemporalTypesTests" /> class.
|
||||
/// </summary>
|
||||
public TemporalTypesTests()
|
||||
{
|
||||
private readonly Shared.TestDbContext _db;
|
||||
private readonly string _dbPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TemporalTypesTests"/> class.
|
||||
/// </summary>
|
||||
public TemporalTypesTests()
|
||||
{
|
||||
_dbPath = $"temporal_test_{Guid.NewGuid()}.db";
|
||||
_db = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases test resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db?.Dispose();
|
||||
if (File.Exists(_dbPath))
|
||||
File.Delete(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies temporal entity collection initialization.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Collection_IsInitialized()
|
||||
{
|
||||
_db.TemporalEntities.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies temporal fields round-trip through insert and lookup.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Insert_And_FindById_Works()
|
||||
{
|
||||
// Arrange
|
||||
var now = DateTime.UtcNow;
|
||||
var offset = DateTimeOffset.UtcNow;
|
||||
var duration = TimeSpan.FromHours(5.5);
|
||||
var birthDate = new DateOnly(1990, 5, 15);
|
||||
var openingTime = new TimeOnly(9, 30, 0);
|
||||
|
||||
var entity = new TemporalEntity
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
Name = "Test Entity",
|
||||
CreatedAt = now,
|
||||
UpdatedAt = offset,
|
||||
LastAccessedAt = offset.AddDays(1),
|
||||
Duration = duration,
|
||||
OptionalDuration = TimeSpan.FromMinutes(30),
|
||||
BirthDate = birthDate,
|
||||
Anniversary = new DateOnly(2020, 6, 10),
|
||||
OpeningTime = openingTime,
|
||||
ClosingTime = new TimeOnly(18, 0, 0)
|
||||
};
|
||||
|
||||
// Act
|
||||
_db.TemporalEntities.Insert(entity);
|
||||
var retrieved = _db.TemporalEntities.FindById(entity.Id);
|
||||
|
||||
// Assert
|
||||
retrieved.ShouldNotBeNull();
|
||||
retrieved.Name.ShouldBe(entity.Name);
|
||||
|
||||
// DateTime comparison (allowing some millisecond precision loss)
|
||||
(retrieved.CreatedAt.Ticks / 10000).ShouldBe(entity.CreatedAt.Ticks / 10000); // millisecond precision
|
||||
|
||||
// DateTimeOffset comparison
|
||||
(retrieved.UpdatedAt.UtcDateTime.Ticks / 10000).ShouldBe(entity.UpdatedAt.UtcDateTime.Ticks / 10000);
|
||||
retrieved.LastAccessedAt.ShouldNotBeNull();
|
||||
(retrieved.LastAccessedAt!.Value.UtcDateTime.Ticks / 10000).ShouldBe(entity.LastAccessedAt!.Value.UtcDateTime.Ticks / 10000);
|
||||
|
||||
// TimeSpan comparison
|
||||
retrieved.Duration.ShouldBe(entity.Duration);
|
||||
retrieved.OptionalDuration.ShouldNotBeNull();
|
||||
retrieved.OptionalDuration!.Value.ShouldBe(entity.OptionalDuration!.Value);
|
||||
|
||||
// DateOnly comparison
|
||||
retrieved.BirthDate.ShouldBe(entity.BirthDate);
|
||||
retrieved.Anniversary.ShouldNotBeNull();
|
||||
retrieved.Anniversary!.Value.ShouldBe(entity.Anniversary!.Value);
|
||||
|
||||
// TimeOnly comparison
|
||||
retrieved.OpeningTime.ShouldBe(entity.OpeningTime);
|
||||
retrieved.ClosingTime.ShouldNotBeNull();
|
||||
retrieved.ClosingTime!.Value.ShouldBe(entity.ClosingTime!.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies insert behavior when optional temporal fields are null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Insert_WithNullOptionalFields_Works()
|
||||
{
|
||||
// Arrange
|
||||
var entity = new TemporalEntity
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
Name = "Minimal Entity",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Duration = TimeSpan.FromHours(1),
|
||||
BirthDate = new DateOnly(1985, 3, 20),
|
||||
OpeningTime = new TimeOnly(8, 0, 0),
|
||||
// Optional fields left null
|
||||
LastAccessedAt = null,
|
||||
OptionalDuration = null,
|
||||
Anniversary = null,
|
||||
ClosingTime = null
|
||||
};
|
||||
|
||||
// Act
|
||||
_db.TemporalEntities.Insert(entity);
|
||||
var retrieved = _db.TemporalEntities.FindById(entity.Id);
|
||||
|
||||
// Assert
|
||||
retrieved.ShouldNotBeNull();
|
||||
retrieved.Name.ShouldBe(entity.Name);
|
||||
retrieved.LastAccessedAt.ShouldBeNull();
|
||||
retrieved.OptionalDuration.ShouldBeNull();
|
||||
retrieved.Anniversary.ShouldBeNull();
|
||||
retrieved.ClosingTime.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies temporal entity updates persist correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Update_Works()
|
||||
{
|
||||
// Arrange
|
||||
var entity = new TemporalEntity
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
Name = "Original",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Duration = TimeSpan.FromHours(1),
|
||||
BirthDate = new DateOnly(1990, 1, 1),
|
||||
OpeningTime = new TimeOnly(9, 0, 0)
|
||||
};
|
||||
|
||||
_db.TemporalEntities.Insert(entity);
|
||||
|
||||
// Act - Update temporal fields
|
||||
entity.Name = "Updated";
|
||||
entity.UpdatedAt = DateTimeOffset.UtcNow.AddDays(1);
|
||||
entity.Duration = TimeSpan.FromHours(2);
|
||||
entity.BirthDate = new DateOnly(1991, 2, 2);
|
||||
entity.OpeningTime = new TimeOnly(10, 0, 0);
|
||||
|
||||
_db.TemporalEntities.Update(entity);
|
||||
var retrieved = _db.TemporalEntities.FindById(entity.Id);
|
||||
|
||||
// Assert
|
||||
retrieved.ShouldNotBeNull();
|
||||
retrieved.Name.ShouldBe("Updated");
|
||||
retrieved.Duration.ShouldBe(entity.Duration);
|
||||
retrieved.BirthDate.ShouldBe(entity.BirthDate);
|
||||
retrieved.OpeningTime.ShouldBe(entity.OpeningTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies querying temporal entities by temporal fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Query_Works()
|
||||
{
|
||||
// Arrange
|
||||
var birthDate1 = new DateOnly(1990, 1, 1);
|
||||
var birthDate2 = new DateOnly(1995, 6, 15);
|
||||
|
||||
var entity1 = new TemporalEntity
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
Name = "Person 1",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Duration = TimeSpan.FromHours(1),
|
||||
BirthDate = birthDate1,
|
||||
OpeningTime = new TimeOnly(9, 0, 0)
|
||||
};
|
||||
|
||||
var entity2 = new TemporalEntity
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
Name = "Person 2",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Duration = TimeSpan.FromHours(2),
|
||||
BirthDate = birthDate2,
|
||||
OpeningTime = new TimeOnly(10, 0, 0)
|
||||
};
|
||||
|
||||
_db.TemporalEntities.Insert(entity1);
|
||||
_db.TemporalEntities.Insert(entity2);
|
||||
|
||||
// Act
|
||||
var results = _db.TemporalEntities.AsQueryable()
|
||||
.Where(e => e.BirthDate == birthDate1)
|
||||
.ToList();
|
||||
|
||||
// Assert
|
||||
results.Count().ShouldBe(1);
|
||||
results[0].Name.ShouldBe("Person 1");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies edge-case TimeSpan values are persisted correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TimeSpan_EdgeCases_Work()
|
||||
{
|
||||
// Arrange - Test various TimeSpan values
|
||||
var entity = new TemporalEntity
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
Name = "TimeSpan Test",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Duration = TimeSpan.Zero,
|
||||
OptionalDuration = TimeSpan.MaxValue,
|
||||
BirthDate = DateOnly.MinValue,
|
||||
OpeningTime = TimeOnly.MinValue
|
||||
};
|
||||
|
||||
// Act
|
||||
_db.TemporalEntities.Insert(entity);
|
||||
var retrieved = _db.TemporalEntities.FindById(entity.Id);
|
||||
|
||||
// Assert
|
||||
retrieved.ShouldNotBeNull();
|
||||
retrieved.Duration.ShouldBe(TimeSpan.Zero);
|
||||
retrieved.OptionalDuration.ShouldNotBeNull();
|
||||
retrieved.OptionalDuration!.Value.ShouldBe(TimeSpan.MaxValue);
|
||||
retrieved.BirthDate.ShouldBe(DateOnly.MinValue);
|
||||
retrieved.OpeningTime.ShouldBe(TimeOnly.MinValue);
|
||||
}
|
||||
_dbPath = $"temporal_test_{Guid.NewGuid()}.db";
|
||||
_db = new TestDbContext(_dbPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases test resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db?.Dispose();
|
||||
if (File.Exists(_dbPath))
|
||||
File.Delete(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies temporal entity collection initialization.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Collection_IsInitialized()
|
||||
{
|
||||
_db.TemporalEntities.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies temporal fields round-trip through insert and lookup.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Insert_And_FindById_Works()
|
||||
{
|
||||
// Arrange
|
||||
var now = DateTime.UtcNow;
|
||||
var offset = DateTimeOffset.UtcNow;
|
||||
var duration = TimeSpan.FromHours(5.5);
|
||||
var birthDate = new DateOnly(1990, 5, 15);
|
||||
var openingTime = new TimeOnly(9, 30, 0);
|
||||
|
||||
var entity = new TemporalEntity
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
Name = "Test Entity",
|
||||
CreatedAt = now,
|
||||
UpdatedAt = offset,
|
||||
LastAccessedAt = offset.AddDays(1),
|
||||
Duration = duration,
|
||||
OptionalDuration = TimeSpan.FromMinutes(30),
|
||||
BirthDate = birthDate,
|
||||
Anniversary = new DateOnly(2020, 6, 10),
|
||||
OpeningTime = openingTime,
|
||||
ClosingTime = new TimeOnly(18, 0, 0)
|
||||
};
|
||||
|
||||
// Act
|
||||
_db.TemporalEntities.Insert(entity);
|
||||
var retrieved = _db.TemporalEntities.FindById(entity.Id);
|
||||
|
||||
// Assert
|
||||
retrieved.ShouldNotBeNull();
|
||||
retrieved.Name.ShouldBe(entity.Name);
|
||||
|
||||
// DateTime comparison (allowing some millisecond precision loss)
|
||||
(retrieved.CreatedAt.Ticks / 10000).ShouldBe(entity.CreatedAt.Ticks / 10000); // millisecond precision
|
||||
|
||||
// DateTimeOffset comparison
|
||||
(retrieved.UpdatedAt.UtcDateTime.Ticks / 10000).ShouldBe(entity.UpdatedAt.UtcDateTime.Ticks / 10000);
|
||||
retrieved.LastAccessedAt.ShouldNotBeNull();
|
||||
(retrieved.LastAccessedAt!.Value.UtcDateTime.Ticks / 10000).ShouldBe(
|
||||
entity.LastAccessedAt!.Value.UtcDateTime.Ticks / 10000);
|
||||
|
||||
// TimeSpan comparison
|
||||
retrieved.Duration.ShouldBe(entity.Duration);
|
||||
retrieved.OptionalDuration.ShouldNotBeNull();
|
||||
retrieved.OptionalDuration!.Value.ShouldBe(entity.OptionalDuration!.Value);
|
||||
|
||||
// DateOnly comparison
|
||||
retrieved.BirthDate.ShouldBe(entity.BirthDate);
|
||||
retrieved.Anniversary.ShouldNotBeNull();
|
||||
retrieved.Anniversary!.Value.ShouldBe(entity.Anniversary!.Value);
|
||||
|
||||
// TimeOnly comparison
|
||||
retrieved.OpeningTime.ShouldBe(entity.OpeningTime);
|
||||
retrieved.ClosingTime.ShouldNotBeNull();
|
||||
retrieved.ClosingTime!.Value.ShouldBe(entity.ClosingTime!.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies insert behavior when optional temporal fields are null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Insert_WithNullOptionalFields_Works()
|
||||
{
|
||||
// Arrange
|
||||
var entity = new TemporalEntity
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
Name = "Minimal Entity",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Duration = TimeSpan.FromHours(1),
|
||||
BirthDate = new DateOnly(1985, 3, 20),
|
||||
OpeningTime = new TimeOnly(8, 0, 0),
|
||||
// Optional fields left null
|
||||
LastAccessedAt = null,
|
||||
OptionalDuration = null,
|
||||
Anniversary = null,
|
||||
ClosingTime = null
|
||||
};
|
||||
|
||||
// Act
|
||||
_db.TemporalEntities.Insert(entity);
|
||||
var retrieved = _db.TemporalEntities.FindById(entity.Id);
|
||||
|
||||
// Assert
|
||||
retrieved.ShouldNotBeNull();
|
||||
retrieved.Name.ShouldBe(entity.Name);
|
||||
retrieved.LastAccessedAt.ShouldBeNull();
|
||||
retrieved.OptionalDuration.ShouldBeNull();
|
||||
retrieved.Anniversary.ShouldBeNull();
|
||||
retrieved.ClosingTime.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies temporal entity updates persist correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Update_Works()
|
||||
{
|
||||
// Arrange
|
||||
var entity = new TemporalEntity
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
Name = "Original",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Duration = TimeSpan.FromHours(1),
|
||||
BirthDate = new DateOnly(1990, 1, 1),
|
||||
OpeningTime = new TimeOnly(9, 0, 0)
|
||||
};
|
||||
|
||||
_db.TemporalEntities.Insert(entity);
|
||||
|
||||
// Act - Update temporal fields
|
||||
entity.Name = "Updated";
|
||||
entity.UpdatedAt = DateTimeOffset.UtcNow.AddDays(1);
|
||||
entity.Duration = TimeSpan.FromHours(2);
|
||||
entity.BirthDate = new DateOnly(1991, 2, 2);
|
||||
entity.OpeningTime = new TimeOnly(10, 0, 0);
|
||||
|
||||
_db.TemporalEntities.Update(entity);
|
||||
var retrieved = _db.TemporalEntities.FindById(entity.Id);
|
||||
|
||||
// Assert
|
||||
retrieved.ShouldNotBeNull();
|
||||
retrieved.Name.ShouldBe("Updated");
|
||||
retrieved.Duration.ShouldBe(entity.Duration);
|
||||
retrieved.BirthDate.ShouldBe(entity.BirthDate);
|
||||
retrieved.OpeningTime.ShouldBe(entity.OpeningTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies querying temporal entities by temporal fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Query_Works()
|
||||
{
|
||||
// Arrange
|
||||
var birthDate1 = new DateOnly(1990, 1, 1);
|
||||
var birthDate2 = new DateOnly(1995, 6, 15);
|
||||
|
||||
var entity1 = new TemporalEntity
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
Name = "Person 1",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Duration = TimeSpan.FromHours(1),
|
||||
BirthDate = birthDate1,
|
||||
OpeningTime = new TimeOnly(9, 0, 0)
|
||||
};
|
||||
|
||||
var entity2 = new TemporalEntity
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
Name = "Person 2",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Duration = TimeSpan.FromHours(2),
|
||||
BirthDate = birthDate2,
|
||||
OpeningTime = new TimeOnly(10, 0, 0)
|
||||
};
|
||||
|
||||
_db.TemporalEntities.Insert(entity1);
|
||||
_db.TemporalEntities.Insert(entity2);
|
||||
|
||||
// Act
|
||||
var results = _db.TemporalEntities.AsQueryable()
|
||||
.Where(e => e.BirthDate == birthDate1)
|
||||
.ToList();
|
||||
|
||||
// Assert
|
||||
results.Count().ShouldBe(1);
|
||||
results[0].Name.ShouldBe("Person 1");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies edge-case TimeSpan values are persisted correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TimeSpan_EdgeCases_Work()
|
||||
{
|
||||
// Arrange - Test various TimeSpan values
|
||||
var entity = new TemporalEntity
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
Name = "TimeSpan Test",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Duration = TimeSpan.Zero,
|
||||
OptionalDuration = TimeSpan.MaxValue,
|
||||
BirthDate = DateOnly.MinValue,
|
||||
OpeningTime = TimeOnly.MinValue
|
||||
};
|
||||
|
||||
// Act
|
||||
_db.TemporalEntities.Insert(entity);
|
||||
var retrieved = _db.TemporalEntities.FindById(entity.Id);
|
||||
|
||||
// Assert
|
||||
retrieved.ShouldNotBeNull();
|
||||
retrieved.Duration.ShouldBe(TimeSpan.Zero);
|
||||
retrieved.OptionalDuration.ShouldNotBeNull();
|
||||
retrieved.OptionalDuration!.Value.ShouldBe(TimeSpan.MaxValue);
|
||||
retrieved.BirthDate.ShouldBe(DateOnly.MinValue);
|
||||
retrieved.OpeningTime.ShouldBe(TimeOnly.MinValue);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,11 @@
|
||||
using ZB.MOM.WW.CBDD.Bson;
|
||||
using ZB.MOM.WW.CBDD.Core.Collections;
|
||||
using Xunit;
|
||||
using System.Linq;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class VisibilityTests
|
||||
{
|
||||
public class VisibilityEntity
|
||||
{
|
||||
// Should be included
|
||||
/// <summary>
|
||||
/// Gets or sets the normal prop.
|
||||
/// </summary>
|
||||
public int NormalProp { get; set; }
|
||||
|
||||
// Should be included (serialization usually writes it)
|
||||
/// <summary>
|
||||
/// Gets or sets the private set prop.
|
||||
/// </summary>
|
||||
public int PrivateSetProp { get; private set; }
|
||||
|
||||
// Should be included
|
||||
/// <summary>
|
||||
/// Gets or sets the init prop.
|
||||
/// </summary>
|
||||
public int InitProp { get; init; }
|
||||
|
||||
// Fields - typically included in BSON if public, but reflection need GetFields
|
||||
public string PublicField = string.Empty;
|
||||
|
||||
// Should NOT be included
|
||||
private int _privateField;
|
||||
|
||||
// Helper to set private
|
||||
/// <summary>
|
||||
/// Tests set private.
|
||||
/// </summary>
|
||||
/// <param name="val">Value assigned to the private field.</param>
|
||||
public void SetPrivate(int val) => _privateField = val;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests generate schema visibility checks.
|
||||
/// Tests generate schema visibility checks.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateSchema_VisibilityChecks()
|
||||
@@ -60,4 +23,41 @@ public class VisibilityTests
|
||||
|
||||
schema.Fields.ShouldNotContain(f => f.Name == "_privatefield");
|
||||
}
|
||||
}
|
||||
|
||||
public class VisibilityEntity
|
||||
{
|
||||
// Should NOT be included
|
||||
private int _privateField;
|
||||
|
||||
// Fields - typically included in BSON if public, but reflection need GetFields
|
||||
public string PublicField = string.Empty;
|
||||
|
||||
// Should be included
|
||||
/// <summary>
|
||||
/// Gets or sets the normal prop.
|
||||
/// </summary>
|
||||
public int NormalProp { get; set; }
|
||||
|
||||
// Should be included (serialization usually writes it)
|
||||
/// <summary>
|
||||
/// Gets or sets the private set prop.
|
||||
/// </summary>
|
||||
public int PrivateSetProp { get; private set; }
|
||||
|
||||
// Should be included
|
||||
/// <summary>
|
||||
/// Gets or sets the init prop.
|
||||
/// </summary>
|
||||
public int InitProp { get; init; }
|
||||
|
||||
// Helper to set private
|
||||
/// <summary>
|
||||
/// Tests set private.
|
||||
/// </summary>
|
||||
/// <param name="val">Value assigned to the private field.</param>
|
||||
public void SetPrivate(int val)
|
||||
{
|
||||
_privateField = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user