Reformat/cleanup
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m10s

This commit is contained in:
Joseph Doherty
2026-02-21 07:53:53 -05:00
parent c6f6d9329a
commit 7ebc2cb567
160 changed files with 7258 additions and 7262 deletions

View File

@@ -1,30 +1,28 @@
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.CBDDC.Core;
using ZB.MOM.WW.CBDDC.Core.Network;
using ZB.MOM.WW.CBDDC.Core.Storage;
using ZB.MOM.WW.CBDDC.Core.Sync;
using ZB.MOM.WW.CBDDC.Persistence.BLite;
using Microsoft.Extensions.Logging.Abstractions;
using System.Text.Json;
using Xunit;
using ZB.MOM.WW.CBDDC.Persistence;
using ZB.MOM.WW.CBDDC.Persistence.BLite;
namespace ZB.MOM.WW.CBDDC.Sample.Console.Tests;
/// <summary>
/// Tests for BLite persistence stores: Export, Import, Merge, Drop operations.
/// Tests for BLite persistence stores: Export, Import, Merge, Drop operations.
/// </summary>
public class BLiteStoreExportImportTests : IDisposable
{
private readonly string _testDbPath;
private readonly IPeerNodeConfigurationProvider _configProvider;
private readonly SampleDbContext _context;
private readonly SampleDocumentStore _documentStore;
private readonly BLiteOplogStore<SampleDbContext> _oplogStore;
private readonly BLitePeerConfigurationStore<SampleDbContext> _peerConfigStore;
private readonly BLiteSnapshotMetadataStore<SampleDbContext> _snapshotMetadataStore;
private readonly IPeerNodeConfigurationProvider _configProvider;
private readonly string _testDbPath;
/// <summary>
/// Initializes a new instance of the <see cref="BLiteStoreExportImportTests"/> class.
/// Initializes a new instance of the <see cref="BLiteStoreExportImportTests" /> class.
/// </summary>
public BLiteStoreExportImportTests()
{
@@ -33,7 +31,8 @@ public class BLiteStoreExportImportTests : IDisposable
_configProvider = CreateConfigProvider("test-node");
var vectorClock = new VectorClockService();
_documentStore = new SampleDocumentStore(_context, _configProvider, vectorClock, NullLogger<SampleDocumentStore>.Instance);
_documentStore = new SampleDocumentStore(_context, _configProvider, vectorClock,
NullLogger<SampleDocumentStore>.Instance);
_snapshotMetadataStore = new BLiteSnapshotMetadataStore<SampleDbContext>(
_context, NullLogger<BLiteSnapshotMetadataStore<SampleDbContext>>.Instance);
_oplogStore = new BLiteOplogStore<SampleDbContext>(
@@ -45,10 +44,42 @@ public class BLiteStoreExportImportTests : IDisposable
_context, NullLogger<BLitePeerConfigurationStore<SampleDbContext>>.Instance);
}
/// <summary>
/// Disposes test resources and removes the temporary database file.
/// </summary>
public void Dispose()
{
_documentStore?.Dispose();
_context?.Dispose();
if (File.Exists(_testDbPath))
try
{
File.Delete(_testDbPath);
}
catch
{
}
}
private static IPeerNodeConfigurationProvider CreateConfigProvider(string nodeId)
{
var configProvider = Substitute.For<IPeerNodeConfigurationProvider>();
configProvider.GetConfiguration().Returns(new PeerNodeConfiguration
{
NodeId = nodeId,
TcpPort = 5000,
AuthToken = "test-token",
OplogRetentionHours = 24,
MaintenanceIntervalMinutes = 60
});
return configProvider;
}
#region OplogStore Tests
/// <summary>
/// Verifies that exporting oplog entries returns all persisted records.
/// Verifies that exporting oplog entries returns all persisted records.
/// </summary>
[Fact]
public async Task OplogStore_ExportAsync_ReturnsAllEntries()
@@ -69,7 +100,7 @@ public class BLiteStoreExportImportTests : IDisposable
}
/// <summary>
/// Verifies that importing oplog entries adds them to the store.
/// Verifies that importing oplog entries adds them to the store.
/// </summary>
[Fact]
public async Task OplogStore_ImportAsync_AddsEntries()
@@ -92,7 +123,7 @@ public class BLiteStoreExportImportTests : IDisposable
}
/// <summary>
/// Verifies that merging oplog entries adds only entries that are not already present.
/// Verifies that merging oplog entries adds only entries that are not already present.
/// </summary>
[Fact]
public async Task OplogStore_MergeAsync_OnlyAddsNewEntries()
@@ -117,7 +148,7 @@ public class BLiteStoreExportImportTests : IDisposable
}
/// <summary>
/// Verifies that chain range lookup resolves entries by hash and returns the expected range.
/// Verifies that chain range lookup resolves entries by hash and returns the expected range.
/// </summary>
[Fact]
public async Task OplogStore_GetChainRangeAsync_UsesHashLookup()
@@ -126,7 +157,8 @@ public class BLiteStoreExportImportTests : IDisposable
var payload1 = JsonDocument.Parse("{\"test\":\"k1\"}").RootElement;
var payload2 = JsonDocument.Parse("{\"test\":\"k2\"}").RootElement;
var entry1 = new OplogEntry("col1", "k1", OperationType.Put, payload1, new HlcTimestamp(1000, 0, "node1"), "");
var entry2 = new OplogEntry("col1", "k2", OperationType.Put, payload2, new HlcTimestamp(2000, 0, "node1"), entry1.Hash);
var entry2 = new OplogEntry("col1", "k2", OperationType.Put, payload2, new HlcTimestamp(2000, 0, "node1"),
entry1.Hash);
await _oplogStore.AppendOplogEntryAsync(entry1);
await _oplogStore.AppendOplogEntryAsync(entry2);
@@ -141,7 +173,7 @@ public class BLiteStoreExportImportTests : IDisposable
}
/// <summary>
/// Verifies that dropping the oplog store removes all entries.
/// Verifies that dropping the oplog store removes all entries.
/// </summary>
[Fact]
public async Task OplogStore_DropAsync_ClearsAllEntries()
@@ -164,7 +196,7 @@ public class BLiteStoreExportImportTests : IDisposable
#region PeerConfigurationStore Tests
/// <summary>
/// Verifies that exporting peer configurations returns all persisted peers.
/// Verifies that exporting peer configurations returns all persisted peers.
/// </summary>
[Fact]
public async Task PeerConfigStore_ExportAsync_ReturnsAllPeers()
@@ -183,7 +215,7 @@ public class BLiteStoreExportImportTests : IDisposable
}
/// <summary>
/// Verifies that importing peer configurations adds peers to the store.
/// Verifies that importing peer configurations adds peers to the store.
/// </summary>
[Fact]
public async Task PeerConfigStore_ImportAsync_AddsPeers()
@@ -204,7 +236,7 @@ public class BLiteStoreExportImportTests : IDisposable
}
/// <summary>
/// Verifies that merging peer configurations adds only new peers.
/// Verifies that merging peer configurations adds only new peers.
/// </summary>
[Fact]
public async Task PeerConfigStore_MergeAsync_OnlyAddsNewPeers()
@@ -229,7 +261,7 @@ public class BLiteStoreExportImportTests : IDisposable
}
/// <summary>
/// Verifies that dropping peer configurations removes all peers.
/// Verifies that dropping peer configurations removes all peers.
/// </summary>
[Fact]
public async Task PeerConfigStore_DropAsync_ClearsAllPeers()
@@ -252,7 +284,7 @@ public class BLiteStoreExportImportTests : IDisposable
#region SnapshotMetadataStore Tests
/// <summary>
/// Verifies that exporting snapshot metadata returns all persisted metadata entries.
/// Verifies that exporting snapshot metadata returns all persisted metadata entries.
/// </summary>
[Fact]
public async Task SnapshotMetadataStore_ExportAsync_ReturnsAllMetadata()
@@ -273,7 +305,7 @@ public class BLiteStoreExportImportTests : IDisposable
}
/// <summary>
/// Verifies that importing snapshot metadata adds metadata entries to the store.
/// Verifies that importing snapshot metadata adds metadata entries to the store.
/// </summary>
[Fact]
public async Task SnapshotMetadataStore_ImportAsync_AddsMetadata()
@@ -294,7 +326,7 @@ public class BLiteStoreExportImportTests : IDisposable
}
/// <summary>
/// Verifies that merging snapshot metadata adds only entries with new node identifiers.
/// Verifies that merging snapshot metadata adds only entries with new node identifiers.
/// </summary>
[Fact]
public async Task SnapshotMetadataStore_MergeAsync_OnlyAddsNewMetadata()
@@ -318,7 +350,7 @@ public class BLiteStoreExportImportTests : IDisposable
}
/// <summary>
/// Verifies that dropping snapshot metadata removes all metadata entries.
/// Verifies that dropping snapshot metadata removes all metadata entries.
/// </summary>
[Fact]
public async Task SnapshotMetadataStore_DropAsync_ClearsAllMetadata()
@@ -340,7 +372,7 @@ public class BLiteStoreExportImportTests : IDisposable
#region DocumentStore Tests
/// <summary>
/// Verifies that exporting documents returns all persisted documents.
/// Verifies that exporting documents returns all persisted documents.
/// </summary>
[Fact]
public async Task DocumentStore_ExportAsync_ReturnsAllDocuments()
@@ -360,7 +392,7 @@ public class BLiteStoreExportImportTests : IDisposable
}
/// <summary>
/// Verifies that importing documents adds them to the underlying store.
/// Verifies that importing documents adds them to the underlying store.
/// </summary>
[Fact]
public async Task DocumentStore_ImportAsync_AddsDocuments()
@@ -385,7 +417,7 @@ public class BLiteStoreExportImportTests : IDisposable
}
/// <summary>
/// Verifies that document merge behavior honors conflict resolution.
/// Verifies that document merge behavior honors conflict resolution.
/// </summary>
[Fact]
public async Task DocumentStore_MergeAsync_UsesConflictResolution()
@@ -414,7 +446,7 @@ public class BLiteStoreExportImportTests : IDisposable
}
/// <summary>
/// Verifies that dropping documents removes all persisted documents.
/// Verifies that dropping documents removes all persisted documents.
/// </summary>
[Fact]
public async Task DocumentStore_DropAsync_ClearsAllDocuments()
@@ -468,38 +500,10 @@ public class BLiteStoreExportImportTests : IDisposable
private static Document CreateDocument<T>(string collection, string key, T entity) where T : class
{
var json = JsonSerializer.Serialize(entity);
string json = JsonSerializer.Serialize(entity);
var content = JsonDocument.Parse(json).RootElement;
return new Document(collection, key, content, new HlcTimestamp(0, 0, ""), false);
}
#endregion
/// <summary>
/// Disposes test resources and removes the temporary database file.
/// </summary>
public void Dispose()
{
_documentStore?.Dispose();
_context?.Dispose();
if (File.Exists(_testDbPath))
{
try { File.Delete(_testDbPath); } catch { }
}
}
private static IPeerNodeConfigurationProvider CreateConfigProvider(string nodeId)
{
var configProvider = Substitute.For<IPeerNodeConfigurationProvider>();
configProvider.GetConfiguration().Returns(new PeerNodeConfiguration
{
NodeId = nodeId,
TcpPort = 5000,
AuthToken = "test-token",
OplogRetentionHours = 24,
MaintenanceIntervalMinutes = 60
});
return configProvider;
}
}
}

View File

@@ -1,3 +1,3 @@
global using ZB.MOM.WW.CBDDC.Sample.Console;
global using NSubstitute;
global using Shouldly;
global using Shouldly;

View File

@@ -7,12 +7,12 @@ namespace ZB.MOM.WW.CBDDC.Sample.Console.Tests;
public class PeerOplogConfirmationStoreTests : IDisposable
{
private readonly string _testDbPath;
private readonly SampleDbContext _context;
private readonly BLitePeerOplogConfirmationStore<SampleDbContext> _store;
private readonly string _testDbPath;
/// <summary>
/// Initializes a new instance of the <see cref="PeerOplogConfirmationStoreTests"/> class.
/// Initializes a new instance of the <see cref="PeerOplogConfirmationStoreTests" /> class.
/// </summary>
public PeerOplogConfirmationStoreTests()
{
@@ -23,8 +23,22 @@ public class PeerOplogConfirmationStoreTests : IDisposable
NullLogger<BLitePeerOplogConfirmationStore<SampleDbContext>>.Instance);
}
/// <inheritdoc />
public void Dispose()
{
_context?.Dispose();
if (File.Exists(_testDbPath))
try
{
File.Delete(_testDbPath);
}
catch
{
}
}
/// <summary>
/// Verifies that ensuring peer registration multiple times remains idempotent.
/// Verifies that ensuring peer registration multiple times remains idempotent.
/// </summary>
[Fact]
public async Task EnsurePeerRegisteredAsync_IsIdempotent()
@@ -41,7 +55,7 @@ public class PeerOplogConfirmationStoreTests : IDisposable
}
/// <summary>
/// Verifies create, update, and read flows for peer oplog confirmations.
/// Verifies create, update, and read flows for peer oplog confirmations.
/// </summary>
[Fact]
public async Task ConfirmationStore_CrudFlow_Works()
@@ -74,7 +88,7 @@ public class PeerOplogConfirmationStoreTests : IDisposable
}
/// <summary>
/// Verifies that removing peer tracking deactivates tracking records for that peer.
/// Verifies that removing peer tracking deactivates tracking records for that peer.
/// </summary>
[Fact]
public async Task RemovePeerTrackingAsync_DeactivatesPeerTracking()
@@ -95,14 +109,4 @@ public class PeerOplogConfirmationStoreTests : IDisposable
peerARows.ShouldNotBeEmpty();
peerARows.All(x => !x.IsActive).ShouldBeTrue();
}
/// <inheritdoc />
public void Dispose()
{
_context?.Dispose();
if (File.Exists(_testDbPath))
{
try { File.Delete(_testDbPath); } catch { }
}
}
}
}

View File

@@ -1,19 +1,12 @@
using ZB.MOM.WW.CBDDC.Core;
using ZB.MOM.WW.CBDDC.Core.Storage;
using ZB.MOM.WW.CBDDC.Core.Sync;
using ZB.MOM.WW.CBDDC.Persistence.BLite;
using Microsoft.Extensions.Logging.Abstractions;
using System.Text.Json;
namespace ZB.MOM.WW.CBDDC.Sample.Console.Tests;
public class SampleDbContextTests : IDisposable
{
private readonly string _dbPath;
private readonly SampleDbContext _context;
private readonly string _dbPath;
/// <summary>
/// Initializes a new test context backed by a temporary database file.
/// Initializes a new test context backed by a temporary database file.
/// </summary>
public SampleDbContextTests()
{
@@ -22,19 +15,23 @@ public class SampleDbContextTests : IDisposable
}
/// <summary>
/// Releases test resources and removes the temporary database file.
/// Releases test resources and removes the temporary database file.
/// </summary>
public void Dispose()
{
_context?.Dispose();
if (File.Exists(_dbPath))
{
try { File.Delete(_dbPath); } catch { }
}
try
{
File.Delete(_dbPath);
}
catch
{
}
}
/// <summary>
/// Verifies that required collections are initialized in the context.
/// Verifies that required collections are initialized in the context.
/// </summary>
[Fact]
public void Context_ShouldInitializeCollections()
@@ -47,7 +44,7 @@ public class SampleDbContextTests : IDisposable
}
/// <summary>
/// Verifies that inserting a user persists the document.
/// Verifies that inserting a user persists the document.
/// </summary>
[Fact]
public async Task Users_Insert_ShouldPersist()
@@ -74,153 +71,153 @@ public class SampleDbContextTests : IDisposable
}
/// <summary>
/// Verifies that updating a user modifies the existing document.
/// Verifies that updating a user modifies the existing document.
/// </summary>
[Fact]
public async Task Users_Update_ShouldModifyExisting()
{
// Arrange
var user = new User { Id = "user2", Name = "Bob", Age = 25 };
await _context.Users.InsertAsync(user);
await _context.SaveChangesAsync();
// Act
user.Age = 26;
user.Address = new Address { City = "Milan" };
await _context.Users.UpdateAsync(user);
await _context.SaveChangesAsync();
// Assert
var retrieved = _context.Users.FindById("user2");
retrieved.ShouldNotBeNull();
retrieved!.Age.ShouldBe(26);
retrieved.Address?.City.ShouldBe("Milan");
}
// Arrange
var user = new User { Id = "user2", Name = "Bob", Age = 25 };
await _context.Users.InsertAsync(user);
await _context.SaveChangesAsync();
// Act
user.Age = 26;
user.Address = new Address { City = "Milan" };
await _context.Users.UpdateAsync(user);
await _context.SaveChangesAsync();
// Assert
var retrieved = _context.Users.FindById("user2");
retrieved.ShouldNotBeNull();
retrieved!.Age.ShouldBe(26);
retrieved.Address?.City.ShouldBe("Milan");
}
/// <summary>
/// Verifies that deleting a user removes the document.
/// Verifies that deleting a user removes the document.
/// </summary>
[Fact]
public async Task Users_Delete_ShouldRemove()
{
// Arrange
var user = new User { Id = "user3", Name = "Charlie", Age = 35 };
await _context.Users.InsertAsync(user);
await _context.SaveChangesAsync();
// Act
await _context.Users.DeleteAsync("user3");
await _context.SaveChangesAsync();
// Assert
var retrieved = _context.Users.FindById("user3");
retrieved.ShouldBeNull();
}
// Arrange
var user = new User { Id = "user3", Name = "Charlie", Age = 35 };
await _context.Users.InsertAsync(user);
await _context.SaveChangesAsync();
// Act
await _context.Users.DeleteAsync("user3");
await _context.SaveChangesAsync();
// Assert
var retrieved = _context.Users.FindById("user3");
retrieved.ShouldBeNull();
}
/// <summary>
/// Verifies that inserting a todo list with items persists nested data.
/// Verifies that inserting a todo list with items persists nested data.
/// </summary>
[Fact]
public async Task TodoLists_InsertWithItems_ShouldPersist()
{
// Arrange
var todoList = new TodoList
{
Id = "list1",
Name = "Shopping",
Items = new List<TodoItem>
{
new() { Task = "Buy milk", Completed = false },
new() { Task = "Buy bread", Completed = true }
}
};
// Act
await _context.TodoLists.InsertAsync(todoList);
await _context.SaveChangesAsync();
// Assert
var retrieved = _context.TodoLists.FindById("list1");
retrieved.ShouldNotBeNull();
retrieved!.Name.ShouldBe("Shopping");
// Arrange
var todoList = new TodoList
{
Id = "list1",
Name = "Shopping",
Items = new List<TodoItem>
{
new() { Task = "Buy milk", Completed = false },
new() { Task = "Buy bread", Completed = true }
}
};
// Act
await _context.TodoLists.InsertAsync(todoList);
await _context.SaveChangesAsync();
// Assert
var retrieved = _context.TodoLists.FindById("list1");
retrieved.ShouldNotBeNull();
retrieved!.Name.ShouldBe("Shopping");
retrieved.Items.Count.ShouldBe(2);
retrieved.Items.ShouldContain(i => i.Task == "Buy milk" && !i.Completed);
retrieved.Items.ShouldContain(i => i.Task == "Buy bread" && i.Completed);
}
retrieved.Items.ShouldContain(i => i.Task == "Buy milk" && !i.Completed);
retrieved.Items.ShouldContain(i => i.Task == "Buy bread" && i.Completed);
}
/// <summary>
/// Verifies that updating todo items modifies the nested collection.
/// Verifies that updating todo items modifies the nested collection.
/// </summary>
[Fact]
public async Task TodoLists_UpdateItems_ShouldModifyNestedCollection()
{
// Arrange
var todoList = new TodoList
{
Id = "list2",
Name = "Work Tasks",
Items = new List<TodoItem>
{
new() { Task = "Write report", Completed = false }
}
};
await _context.TodoLists.InsertAsync(todoList);
await _context.SaveChangesAsync();
// Act - Mark task as completed and add new task
todoList.Items[0].Completed = true;
todoList.Items.Add(new TodoItem { Task = "Review report", Completed = false });
await _context.TodoLists.UpdateAsync(todoList);
await _context.SaveChangesAsync();
// Assert
var retrieved = _context.TodoLists.FindById("list2");
retrieved.ShouldNotBeNull();
// Arrange
var todoList = new TodoList
{
Id = "list2",
Name = "Work Tasks",
Items = new List<TodoItem>
{
new() { Task = "Write report", Completed = false }
}
};
await _context.TodoLists.InsertAsync(todoList);
await _context.SaveChangesAsync();
// Act - Mark task as completed and add new task
todoList.Items[0].Completed = true;
todoList.Items.Add(new TodoItem { Task = "Review report", Completed = false });
await _context.TodoLists.UpdateAsync(todoList);
await _context.SaveChangesAsync();
// Assert
var retrieved = _context.TodoLists.FindById("list2");
retrieved.ShouldNotBeNull();
retrieved!.Items.Count.ShouldBe(2);
retrieved.Items.First().Completed.ShouldBe(true);
retrieved.Items.Last().Completed.ShouldBe(false);
}
retrieved.Items.First().Completed.ShouldBe(true);
retrieved.Items.Last().Completed.ShouldBe(false);
}
/// <summary>
/// Verifies that querying all users returns all inserted users.
/// Verifies that querying all users returns all inserted users.
/// </summary>
[Fact]
public void Users_FindAll_ShouldReturnAllUsers()
{
// Arrange
_context.Users.InsertAsync(new User { Id = "u1", Name = "User1", Age = 20 }).Wait();
_context.Users.InsertAsync(new User { Id = "u2", Name = "User2", Age = 30 }).Wait();
_context.Users.InsertAsync(new User { Id = "u3", Name = "User3", Age = 40 }).Wait();
_context.SaveChangesAsync().Wait();
// Act
var allUsers = _context.Users.FindAll().ToList();
// Assert
// Arrange
_context.Users.InsertAsync(new User { Id = "u1", Name = "User1", Age = 20 }).Wait();
_context.Users.InsertAsync(new User { Id = "u2", Name = "User2", Age = 30 }).Wait();
_context.Users.InsertAsync(new User { Id = "u3", Name = "User3", Age = 40 }).Wait();
_context.SaveChangesAsync().Wait();
// Act
var allUsers = _context.Users.FindAll().ToList();
// Assert
allUsers.Count.ShouldBe(3);
allUsers.Select(u => u.Name).ShouldContain("User1");
allUsers.Select(u => u.Name).ShouldContain("User2");
allUsers.Select(u => u.Name).ShouldContain("User3");
}
}
/// <summary>
/// Verifies that predicate-based queries return only matching users.
/// Verifies that predicate-based queries return only matching users.
/// </summary>
[Fact]
public void Users_Find_WithPredicate_ShouldFilterCorrectly()
{
// Arrange
_context.Users.InsertAsync(new User { Id = "f1", Name = "Young", Age = 18 }).Wait();
_context.Users.InsertAsync(new User { Id = "f2", Name = "Adult", Age = 30 }).Wait();
_context.Users.InsertAsync(new User { Id = "f3", Name = "Senior", Age = 65 }).Wait();
_context.SaveChangesAsync().Wait();
// Act
var adults = _context.Users.Find(u => u.Age >= 30).ToList();
// Assert
// Arrange
_context.Users.InsertAsync(new User { Id = "f1", Name = "Young", Age = 18 }).Wait();
_context.Users.InsertAsync(new User { Id = "f2", Name = "Adult", Age = 30 }).Wait();
_context.Users.InsertAsync(new User { Id = "f3", Name = "Senior", Age = 65 }).Wait();
_context.SaveChangesAsync().Wait();
// Act
var adults = _context.Users.Find(u => u.Age >= 30).ToList();
// Assert
adults.Count.ShouldBe(2);
adults.Select(u => u.Name).ShouldContain("Adult");
adults.Select(u => u.Name).ShouldContain("Senior");
}
}
}
}

View File

@@ -1,29 +1,27 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.CBDDC.Core;
using ZB.MOM.WW.CBDDC.Core.Network;
using ZB.MOM.WW.CBDDC.Core.Storage;
using ZB.MOM.WW.CBDDC.Core.Sync;
using ZB.MOM.WW.CBDDC.Persistence.BLite;
using Microsoft.Extensions.Logging.Abstractions;
using System.Text.Json;
using System.Text.Json.Nodes;
using Xunit;
using ZB.MOM.WW.CBDDC.Persistence;
using ZB.MOM.WW.CBDDC.Persistence;
using ZB.MOM.WW.CBDDC.Persistence.BLite;
namespace ZB.MOM.WW.CBDDC.Sample.Console.Tests;
public class SnapshotStoreTests : IDisposable
{
private readonly string _testDbPath;
private readonly IPeerNodeConfigurationProvider _configProvider;
private readonly SampleDbContext _context;
private readonly SampleDocumentStore _documentStore;
private readonly BLiteOplogStore<SampleDbContext> _oplogStore;
private readonly BLitePeerConfigurationStore<SampleDbContext> _peerConfigStore;
private readonly BLitePeerOplogConfirmationStore<SampleDbContext> _peerConfirmationStore;
private readonly SnapshotStore _snapshotStore;
private readonly IPeerNodeConfigurationProvider _configProvider;
private readonly SampleDocumentStore _documentStore;
private readonly BLiteOplogStore<SampleDbContext> _oplogStore;
private readonly BLitePeerConfigurationStore<SampleDbContext> _peerConfigStore;
private readonly BLitePeerOplogConfirmationStore<SampleDbContext> _peerConfirmationStore;
private readonly SnapshotStore _snapshotStore;
private readonly string _testDbPath;
/// <summary>
/// Initializes a new instance of the <see cref="SnapshotStoreTests"/> class.
/// Initializes a new instance of the <see cref="SnapshotStoreTests" /> class.
/// </summary>
public SnapshotStoreTests()
{
@@ -32,7 +30,8 @@ public class SnapshotStoreTests : IDisposable
_configProvider = CreateConfigProvider("test-node");
var vectorClock = new VectorClockService();
_documentStore = new SampleDocumentStore(_context, _configProvider, vectorClock, NullLogger<SampleDocumentStore>.Instance);
_documentStore = new SampleDocumentStore(_context, _configProvider, vectorClock,
NullLogger<SampleDocumentStore>.Instance);
var snapshotMetadataStore = new BLiteSnapshotMetadataStore<SampleDbContext>(
_context,
NullLogger<BLiteSnapshotMetadataStore<SampleDbContext>>.Instance);
@@ -43,25 +42,43 @@ public class SnapshotStoreTests : IDisposable
vectorClock,
snapshotMetadataStore,
NullLogger<BLiteOplogStore<SampleDbContext>>.Instance);
_peerConfigStore = new BLitePeerConfigurationStore<SampleDbContext>(
_context,
NullLogger<BLitePeerConfigurationStore<SampleDbContext>>.Instance);
_peerConfirmationStore = new BLitePeerOplogConfirmationStore<SampleDbContext>(
_context,
NullLogger<BLitePeerOplogConfirmationStore<SampleDbContext>>.Instance);
_snapshotStore = new SnapshotStore(
_documentStore,
_peerConfigStore,
_oplogStore,
new LastWriteWinsConflictResolver(),
NullLogger<SnapshotStore>.Instance,
_peerConfirmationStore);
_peerConfigStore = new BLitePeerConfigurationStore<SampleDbContext>(
_context,
NullLogger<BLitePeerConfigurationStore<SampleDbContext>>.Instance);
_peerConfirmationStore = new BLitePeerOplogConfirmationStore<SampleDbContext>(
_context,
NullLogger<BLitePeerOplogConfirmationStore<SampleDbContext>>.Instance);
_snapshotStore = new SnapshotStore(
_documentStore,
_peerConfigStore,
_oplogStore,
new LastWriteWinsConflictResolver(),
NullLogger<SnapshotStore>.Instance,
_peerConfirmationStore);
}
/// <summary>
/// Releases resources created for test execution.
/// </summary>
public void Dispose()
{
_documentStore?.Dispose();
_context?.Dispose();
if (File.Exists(_testDbPath))
try
{
File.Delete(_testDbPath);
}
catch
{
}
}
/// <summary>
/// Verifies that creating a snapshot writes valid JSON to the output stream.
/// Verifies that creating a snapshot writes valid JSON to the output stream.
/// </summary>
[Fact]
public async Task CreateSnapshotAsync_WritesValidJsonToStream()
@@ -80,7 +97,7 @@ public class SnapshotStoreTests : IDisposable
// Reset stream position and verify JSON is valid
stream.Position = 0;
var json = await new StreamReader(stream).ReadToEndAsync();
string json = await new StreamReader(stream).ReadToEndAsync();
string.IsNullOrWhiteSpace(json).ShouldBeFalse("Snapshot JSON should not be empty");
json.Trim().ShouldStartWith("{");
@@ -90,14 +107,15 @@ public class SnapshotStoreTests : IDisposable
doc.ShouldNotBeNull();
// Verify structure
doc.RootElement.TryGetProperty("Version", out _).ShouldBeTrue("Should have Version property");
doc.RootElement.TryGetProperty("Documents", out _).ShouldBeTrue("Should have Documents property");
doc.RootElement.TryGetProperty("Oplog", out _).ShouldBeTrue("Should have Oplog property");
doc.RootElement.TryGetProperty("PeerConfirmations", out _).ShouldBeTrue("Should have PeerConfirmations property");
doc.RootElement.TryGetProperty("Version", out _).ShouldBeTrue("Should have Version property");
doc.RootElement.TryGetProperty("Documents", out _).ShouldBeTrue("Should have Documents property");
doc.RootElement.TryGetProperty("Oplog", out _).ShouldBeTrue("Should have Oplog property");
doc.RootElement.TryGetProperty("PeerConfirmations", out _)
.ShouldBeTrue("Should have PeerConfirmations property");
}
/// <summary>
/// Verifies that snapshot creation includes all persisted documents.
/// Verifies that snapshot creation includes all persisted documents.
/// </summary>
[Fact]
public async Task CreateSnapshotAsync_IncludesAllDocuments()
@@ -119,7 +137,7 @@ public class SnapshotStoreTests : IDisposable
// Assert
stream.Position = 0;
var json = await new StreamReader(stream).ReadToEndAsync();
string json = await new StreamReader(stream).ReadToEndAsync();
var doc = JsonDocument.Parse(json);
var documents = doc.RootElement.GetProperty("Documents");
@@ -127,38 +145,39 @@ public class SnapshotStoreTests : IDisposable
}
/// <summary>
/// Verifies that creating and replacing a snapshot preserves document data.
/// Verifies that creating and replacing a snapshot preserves document data.
/// </summary>
[Fact]
public async Task RoundTrip_CreateAndReplace_PreservesData()
{
// Arrange - Add data to source
var originalUser = new User { Id = "user-rt", Name = "RoundTrip User", Age = 42 };
await _context.Users.InsertAsync(originalUser);
await _peerConfirmationStore.UpdateConfirmationAsync(
"peer-rt",
"source-rt",
new HlcTimestamp(500, 2, "source-rt"),
"hash-rt");
await _context.SaveChangesAsync();
public async Task RoundTrip_CreateAndReplace_PreservesData()
{
// Arrange - Add data to source
var originalUser = new User { Id = "user-rt", Name = "RoundTrip User", Age = 42 };
await _context.Users.InsertAsync(originalUser);
await _peerConfirmationStore.UpdateConfirmationAsync(
"peer-rt",
"source-rt",
new HlcTimestamp(500, 2, "source-rt"),
"hash-rt");
await _context.SaveChangesAsync();
// Create snapshot
using var snapshotStream = new MemoryStream();
await _snapshotStore.CreateSnapshotAsync(snapshotStream);
snapshotStream.Position = 0;
var snapshotJson = await new StreamReader(snapshotStream).ReadToEndAsync();
var snapshotDoc = JsonDocument.Parse(snapshotJson);
snapshotDoc.RootElement.GetProperty("PeerConfirmations").GetArrayLength().ShouldBe(1);
snapshotStream.Position = 0;
// Create snapshot
using var snapshotStream = new MemoryStream();
await _snapshotStore.CreateSnapshotAsync(snapshotStream);
snapshotStream.Position = 0;
string snapshotJson = await new StreamReader(snapshotStream).ReadToEndAsync();
var snapshotDoc = JsonDocument.Parse(snapshotJson);
snapshotDoc.RootElement.GetProperty("PeerConfirmations").GetArrayLength().ShouldBe(1);
snapshotStream.Position = 0;
// Create a new context/stores (simulating a different node)
var newDbPath = Path.Combine(Path.GetTempPath(), $"test-snapshot-target-{Guid.NewGuid()}.blite");
string newDbPath = Path.Combine(Path.GetTempPath(), $"test-snapshot-target-{Guid.NewGuid()}.blite");
try
{
using var newContext = new SampleDbContext(newDbPath);
var newConfigProvider = CreateConfigProvider("test-new-node");
var newVectorClock = new VectorClockService();
var newDocStore = new SampleDocumentStore(newContext, newConfigProvider, newVectorClock, NullLogger<SampleDocumentStore>.Instance);
var newDocStore = new SampleDocumentStore(newContext, newConfigProvider, newVectorClock,
NullLogger<SampleDocumentStore>.Instance);
var newSnapshotMetaStore = new BLiteSnapshotMetadataStore<SampleDbContext>(
newContext, NullLogger<BLiteSnapshotMetadataStore<SampleDbContext>>.Instance);
var newOplogStore = new BLiteOplogStore<SampleDbContext>(
@@ -166,66 +185,72 @@ public class SnapshotStoreTests : IDisposable
newVectorClock,
newSnapshotMetaStore,
NullLogger<BLiteOplogStore<SampleDbContext>>.Instance);
var newPeerStore = new BLitePeerConfigurationStore<SampleDbContext>(
newContext, NullLogger<BLitePeerConfigurationStore<SampleDbContext>>.Instance);
var newPeerConfirmationStore = new BLitePeerOplogConfirmationStore<SampleDbContext>(
newContext,
NullLogger<BLitePeerOplogConfirmationStore<SampleDbContext>>.Instance);
var newSnapshotStore = new SnapshotStore(
newDocStore,
newPeerStore,
newOplogStore,
new LastWriteWinsConflictResolver(),
NullLogger<SnapshotStore>.Instance,
newPeerConfirmationStore);
var newPeerStore = new BLitePeerConfigurationStore<SampleDbContext>(
newContext, NullLogger<BLitePeerConfigurationStore<SampleDbContext>>.Instance);
var newPeerConfirmationStore = new BLitePeerOplogConfirmationStore<SampleDbContext>(
newContext,
NullLogger<BLitePeerOplogConfirmationStore<SampleDbContext>>.Instance);
var newSnapshotStore = new SnapshotStore(
newDocStore,
newPeerStore,
newOplogStore,
new LastWriteWinsConflictResolver(),
NullLogger<SnapshotStore>.Instance,
newPeerConfirmationStore);
// Act - Replace database with snapshot
await newSnapshotStore.ReplaceDatabaseAsync(snapshotStream);
// Assert - Data should be restored
var restoredUser = newContext.Users.FindById("user-rt");
restoredUser.ShouldNotBeNull();
restoredUser.Name.ShouldBe("RoundTrip User");
restoredUser.Age.ShouldBe(42);
var restoredConfirmations = (await newPeerConfirmationStore.GetConfirmationsAsync()).ToList();
restoredConfirmations.Count.ShouldBe(1);
restoredConfirmations[0].PeerNodeId.ShouldBe("peer-rt");
restoredConfirmations[0].SourceNodeId.ShouldBe("source-rt");
restoredConfirmations[0].ConfirmedWall.ShouldBe(500);
restoredConfirmations[0].ConfirmedLogic.ShouldBe(2);
restoredConfirmations[0].ConfirmedHash.ShouldBe("hash-rt");
}
finally
{
restoredUser.ShouldNotBeNull();
restoredUser.Name.ShouldBe("RoundTrip User");
restoredUser.Age.ShouldBe(42);
var restoredConfirmations = (await newPeerConfirmationStore.GetConfirmationsAsync()).ToList();
restoredConfirmations.Count.ShouldBe(1);
restoredConfirmations[0].PeerNodeId.ShouldBe("peer-rt");
restoredConfirmations[0].SourceNodeId.ShouldBe("source-rt");
restoredConfirmations[0].ConfirmedWall.ShouldBe(500);
restoredConfirmations[0].ConfirmedLogic.ShouldBe(2);
restoredConfirmations[0].ConfirmedHash.ShouldBe("hash-rt");
}
finally
{
if (File.Exists(newDbPath))
try { File.Delete(newDbPath); } catch { }
try
{
File.Delete(newDbPath);
}
catch
{
}
}
}
/// <summary>
/// Verifies that merging a snapshot preserves existing data and adds new data.
/// Verifies that merging a snapshot preserves existing data and adds new data.
/// </summary>
[Fact]
public async Task MergeSnapshotAsync_MergesWithExistingData()
{
// Arrange - Add initial data
await _context.Users.InsertAsync(new User { Id = "existing", Name = "Existing User", Age = 30 });
await _peerConfirmationStore.UpdateConfirmationAsync(
"peer-merge",
"source-a",
new HlcTimestamp(100, 0, "source-a"),
"target-hash-old");
await _peerConfirmationStore.UpdateConfirmationAsync(
"peer-local-only",
"source-local",
new HlcTimestamp(50, 0, "source-local"),
"target-local-hash");
await _context.SaveChangesAsync();
public async Task MergeSnapshotAsync_MergesWithExistingData()
{
// Arrange - Add initial data
await _context.Users.InsertAsync(new User { Id = "existing", Name = "Existing User", Age = 30 });
await _peerConfirmationStore.UpdateConfirmationAsync(
"peer-merge",
"source-a",
new HlcTimestamp(100, 0, "source-a"),
"target-hash-old");
await _peerConfirmationStore.UpdateConfirmationAsync(
"peer-local-only",
"source-local",
new HlcTimestamp(50, 0, "source-local"),
"target-local-hash");
await _context.SaveChangesAsync();
// Create snapshot with different data
var sourceDbPath = Path.Combine(Path.GetTempPath(), $"test-snapshot-source-{Guid.NewGuid()}.blite");
string sourceDbPath = Path.Combine(Path.GetTempPath(), $"test-snapshot-source-{Guid.NewGuid()}.blite");
MemoryStream snapshotStream;
try
@@ -236,7 +261,8 @@ public class SnapshotStoreTests : IDisposable
var sourceConfigProvider = CreateConfigProvider("test-source-node");
var sourceVectorClock = new VectorClockService();
var sourceDocStore = new SampleDocumentStore(sourceContext, sourceConfigProvider, sourceVectorClock, NullLogger<SampleDocumentStore>.Instance);
var sourceDocStore = new SampleDocumentStore(sourceContext, sourceConfigProvider, sourceVectorClock,
NullLogger<SampleDocumentStore>.Instance);
var sourceSnapshotMetaStore = new BLiteSnapshotMetadataStore<SampleDbContext>(
sourceContext, NullLogger<BLiteSnapshotMetadataStore<SampleDbContext>>.Instance);
var sourceOplogStore = new BLiteOplogStore<SampleDbContext>(
@@ -244,29 +270,29 @@ public class SnapshotStoreTests : IDisposable
sourceVectorClock,
sourceSnapshotMetaStore,
NullLogger<BLiteOplogStore<SampleDbContext>>.Instance);
var sourcePeerStore = new BLitePeerConfigurationStore<SampleDbContext>(
sourceContext, NullLogger<BLitePeerConfigurationStore<SampleDbContext>>.Instance);
var sourcePeerConfirmationStore = new BLitePeerOplogConfirmationStore<SampleDbContext>(
sourceContext,
NullLogger<BLitePeerOplogConfirmationStore<SampleDbContext>>.Instance);
await sourcePeerConfirmationStore.UpdateConfirmationAsync(
"peer-merge",
"source-a",
new HlcTimestamp(200, 1, "source-a"),
"source-hash-new");
await sourcePeerConfirmationStore.UpdateConfirmationAsync(
"peer-merge",
"source-b",
new HlcTimestamp(300, 0, "source-b"),
"source-hash-b");
var sourceSnapshotStore = new SnapshotStore(
sourceDocStore,
sourcePeerStore,
sourceOplogStore,
new LastWriteWinsConflictResolver(),
NullLogger<SnapshotStore>.Instance,
sourcePeerConfirmationStore);
var sourcePeerStore = new BLitePeerConfigurationStore<SampleDbContext>(
sourceContext, NullLogger<BLitePeerConfigurationStore<SampleDbContext>>.Instance);
var sourcePeerConfirmationStore = new BLitePeerOplogConfirmationStore<SampleDbContext>(
sourceContext,
NullLogger<BLitePeerOplogConfirmationStore<SampleDbContext>>.Instance);
await sourcePeerConfirmationStore.UpdateConfirmationAsync(
"peer-merge",
"source-a",
new HlcTimestamp(200, 1, "source-a"),
"source-hash-new");
await sourcePeerConfirmationStore.UpdateConfirmationAsync(
"peer-merge",
"source-b",
new HlcTimestamp(300, 0, "source-b"),
"source-hash-b");
var sourceSnapshotStore = new SnapshotStore(
sourceDocStore,
sourcePeerStore,
sourceOplogStore,
new LastWriteWinsConflictResolver(),
NullLogger<SnapshotStore>.Instance,
sourcePeerConfirmationStore);
snapshotStream = new MemoryStream();
await sourceSnapshotStore.CreateSnapshotAsync(snapshotStream);
@@ -275,7 +301,13 @@ public class SnapshotStoreTests : IDisposable
finally
{
if (File.Exists(sourceDbPath))
try { File.Delete(sourceDbPath); } catch { }
try
{
File.Delete(sourceDbPath);
}
catch
{
}
}
// Act - Merge snapshot into existing data
@@ -285,70 +317,71 @@ public class SnapshotStoreTests : IDisposable
var existingUser = _context.Users.FindById("existing");
var newUser = _context.Users.FindById("new-user");
existingUser.ShouldNotBeNull();
newUser.ShouldNotBeNull();
existingUser.Name.ShouldBe("Existing User");
newUser.Name.ShouldBe("New User");
var confirmations = (await _peerConfirmationStore.GetConfirmationsAsync())
.OrderBy(c => c.PeerNodeId)
.ThenBy(c => c.SourceNodeId)
.ToList();
confirmations.Count.ShouldBe(3);
confirmations[0].PeerNodeId.ShouldBe("peer-local-only");
confirmations[0].SourceNodeId.ShouldBe("source-local");
confirmations[0].ConfirmedWall.ShouldBe(50);
confirmations[0].ConfirmedHash.ShouldBe("target-local-hash");
confirmations[1].PeerNodeId.ShouldBe("peer-merge");
confirmations[1].SourceNodeId.ShouldBe("source-a");
confirmations[1].ConfirmedWall.ShouldBe(200);
confirmations[1].ConfirmedLogic.ShouldBe(1);
confirmations[1].ConfirmedHash.ShouldBe("source-hash-new");
confirmations[2].PeerNodeId.ShouldBe("peer-merge");
confirmations[2].SourceNodeId.ShouldBe("source-b");
confirmations[2].ConfirmedWall.ShouldBe(300);
confirmations[2].ConfirmedHash.ShouldBe("source-hash-b");
}
/// <summary>
/// Verifies that replace can consume legacy snapshots that do not include peer confirmations.
/// </summary>
[Fact]
public async Task ReplaceDatabaseAsync_LegacySnapshotWithoutPeerConfirmations_IsSupported()
{
// Arrange
await _context.Users.InsertAsync(new User { Id = "legacy-user", Name = "Legacy User", Age = 33 });
await _context.SaveChangesAsync();
using var snapshotStream = new MemoryStream();
await _snapshotStore.CreateSnapshotAsync(snapshotStream);
snapshotStream.Position = 0;
var snapshotJson = await new StreamReader(snapshotStream).ReadToEndAsync();
var legacySnapshot = JsonNode.Parse(snapshotJson)!.AsObject();
legacySnapshot.Remove("PeerConfirmations");
using var legacyStream = new MemoryStream();
await using (var writer = new Utf8JsonWriter(legacyStream))
{
legacySnapshot.WriteTo(writer);
}
legacyStream.Position = 0;
// Act
await _snapshotStore.ReplaceDatabaseAsync(legacyStream);
// Assert
_context.Users.FindById("legacy-user").ShouldNotBeNull();
(await _peerConfirmationStore.GetConfirmationsAsync()).Count().ShouldBe(0);
}
/// <summary>
/// Verifies that snapshot creation succeeds for an empty database.
/// </summary>
existingUser.ShouldNotBeNull();
newUser.ShouldNotBeNull();
existingUser.Name.ShouldBe("Existing User");
newUser.Name.ShouldBe("New User");
var confirmations = (await _peerConfirmationStore.GetConfirmationsAsync())
.OrderBy(c => c.PeerNodeId)
.ThenBy(c => c.SourceNodeId)
.ToList();
confirmations.Count.ShouldBe(3);
confirmations[0].PeerNodeId.ShouldBe("peer-local-only");
confirmations[0].SourceNodeId.ShouldBe("source-local");
confirmations[0].ConfirmedWall.ShouldBe(50);
confirmations[0].ConfirmedHash.ShouldBe("target-local-hash");
confirmations[1].PeerNodeId.ShouldBe("peer-merge");
confirmations[1].SourceNodeId.ShouldBe("source-a");
confirmations[1].ConfirmedWall.ShouldBe(200);
confirmations[1].ConfirmedLogic.ShouldBe(1);
confirmations[1].ConfirmedHash.ShouldBe("source-hash-new");
confirmations[2].PeerNodeId.ShouldBe("peer-merge");
confirmations[2].SourceNodeId.ShouldBe("source-b");
confirmations[2].ConfirmedWall.ShouldBe(300);
confirmations[2].ConfirmedHash.ShouldBe("source-hash-b");
}
/// <summary>
/// Verifies that replace can consume legacy snapshots that do not include peer confirmations.
/// </summary>
[Fact]
public async Task ReplaceDatabaseAsync_LegacySnapshotWithoutPeerConfirmations_IsSupported()
{
// Arrange
await _context.Users.InsertAsync(new User { Id = "legacy-user", Name = "Legacy User", Age = 33 });
await _context.SaveChangesAsync();
using var snapshotStream = new MemoryStream();
await _snapshotStore.CreateSnapshotAsync(snapshotStream);
snapshotStream.Position = 0;
string snapshotJson = await new StreamReader(snapshotStream).ReadToEndAsync();
var legacySnapshot = JsonNode.Parse(snapshotJson)!.AsObject();
legacySnapshot.Remove("PeerConfirmations");
using var legacyStream = new MemoryStream();
await using (var writer = new Utf8JsonWriter(legacyStream))
{
legacySnapshot.WriteTo(writer);
}
legacyStream.Position = 0;
// Act
await _snapshotStore.ReplaceDatabaseAsync(legacyStream);
// Assert
_context.Users.FindById("legacy-user").ShouldNotBeNull();
(await _peerConfirmationStore.GetConfirmationsAsync()).Count().ShouldBe(0);
}
/// <summary>
/// Verifies that snapshot creation succeeds for an empty database.
/// </summary>
[Fact]
public async Task CreateSnapshotAsync_HandlesEmptyDatabase()
{
@@ -360,7 +393,7 @@ public class SnapshotStoreTests : IDisposable
(stream.Length > 0).ShouldBeTrue();
stream.Position = 0;
var json = await new StreamReader(stream).ReadToEndAsync();
string json = await new StreamReader(stream).ReadToEndAsync();
var doc = JsonDocument.Parse(json);
var documents = doc.RootElement.GetProperty("Documents");
@@ -368,7 +401,7 @@ public class SnapshotStoreTests : IDisposable
}
/// <summary>
/// Verifies that snapshot creation includes oplog entries.
/// Verifies that snapshot creation includes oplog entries.
/// </summary>
[Fact]
public async Task CreateSnapshotAsync_IncludesOplogEntries()
@@ -394,27 +427,13 @@ public class SnapshotStoreTests : IDisposable
// Assert
stream.Position = 0;
var json = await new StreamReader(stream).ReadToEndAsync();
string json = await new StreamReader(stream).ReadToEndAsync();
var doc = JsonDocument.Parse(json);
var oplog = doc.RootElement.GetProperty("Oplog");
(oplog.GetArrayLength() >= 1).ShouldBeTrue("Should have at least one oplog entry");
}
/// <summary>
/// Releases resources created for test execution.
/// </summary>
public void Dispose()
{
_documentStore?.Dispose();
_context?.Dispose();
if (File.Exists(_testDbPath))
{
try { File.Delete(_testDbPath); } catch { }
}
}
private static IPeerNodeConfigurationProvider CreateConfigProvider(string nodeId)
{
var configProvider = Substitute.For<IPeerNodeConfigurationProvider>();
@@ -428,4 +447,4 @@ public class SnapshotStoreTests : IDisposable
});
return configProvider;
}
}
}

View File

@@ -1,32 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>ZB.MOM.WW.CBDDC.Sample.Console.Tests</AssemblyName>
<RootNamespace>ZB.MOM.WW.CBDDC.Sample.Console.Tests</RootNamespace>
<PackageId>ZB.MOM.WW.CBDDC.Sample.Console.Tests</PackageId>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);xUnit1031;xUnit1051</NoWarn>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageReference Include="xunit.v3" Version="3.2.0" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\samples\ZB.MOM.WW.CBDDC.Sample.Console\ZB.MOM.WW.CBDDC.Sample.Console.csproj" />
<ProjectReference Include="..\..\src\ZB.MOM.WW.CBDDC.Persistence\ZB.MOM.WW.CBDDC.Persistence.csproj" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>ZB.MOM.WW.CBDDC.Sample.Console.Tests</AssemblyName>
<RootNamespace>ZB.MOM.WW.CBDDC.Sample.Console.Tests</RootNamespace>
<PackageId>ZB.MOM.WW.CBDDC.Sample.Console.Tests</PackageId>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);xUnit1031;xUnit1051</NoWarn>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4"/>
<PackageReference Include="NSubstitute" Version="5.3.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="Shouldly" Version="4.3.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4"/>
<PackageReference Include="xunit.v3" Version="3.2.0"/>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\samples\ZB.MOM.WW.CBDDC.Sample.Console\ZB.MOM.WW.CBDDC.Sample.Console.csproj"/>
<ProjectReference Include="..\..\src\ZB.MOM.WW.CBDDC.Persistence\ZB.MOM.WW.CBDDC.Persistence.csproj"/>
</ItemGroup>
</Project>