Reformat/cleanup
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m10s
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m10s
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
global using ZB.MOM.WW.CBDDC.Sample.Console;
|
||||
global using NSubstitute;
|
||||
global using Shouldly;
|
||||
global using Shouldly;
|
||||
@@ -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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user