|
| 1 | +using System; |
| 2 | +using Akka.Configuration; |
| 3 | +using Akka.Hosting; |
| 4 | +using FluentAssertions; |
| 5 | +using Microsoft.Extensions.DependencyInjection; |
| 6 | +using Microsoft.Extensions.Hosting; |
| 7 | +using Xunit; |
| 8 | +using Xunit.Abstractions; |
| 9 | +using static Akka.Persistence.Hosting.Tests.UnifiedApiTestResources; |
| 10 | + |
| 11 | +namespace Akka.Persistence.Hosting.Tests; |
| 12 | + |
| 13 | +/// <summary> |
| 14 | +/// Tests that verify builders expose their options, eliminating the need |
| 15 | +/// to pass options explicitly to extension methods like WithConnectivityCheck(). |
| 16 | +/// |
| 17 | +/// This resolves the API design issue identified in https://github.com/akkadotnet/Akka.Hosting/issues/690 |
| 18 | +/// </summary> |
| 19 | +public sealed class BuilderOptionsAccessSpec |
| 20 | +{ |
| 21 | + private readonly ITestOutputHelper _output; |
| 22 | + |
| 23 | + public BuilderOptionsAccessSpec(ITestOutputHelper output) |
| 24 | + { |
| 25 | + _output = output; |
| 26 | + } |
| 27 | + |
| 28 | + [Fact(DisplayName = "Journal builder should expose JournalOptions via Options property")] |
| 29 | + public void JournalBuilderShouldExposeOptions() |
| 30 | + { |
| 31 | + // Arrange |
| 32 | + var services = new ServiceCollection(); |
| 33 | + var builder = new AkkaConfigurationBuilder(services, "test-system"); |
| 34 | + var journalOptions = new TestJournalOptions(isDefault: true) |
| 35 | + { |
| 36 | + Identifier = "test-journal" |
| 37 | + }; |
| 38 | + |
| 39 | + AkkaPersistenceJournalBuilder? capturedBuilder = null; |
| 40 | + |
| 41 | + // Act |
| 42 | + builder.WithJournal(journalOptions, journal => |
| 43 | + { |
| 44 | + capturedBuilder = journal; |
| 45 | + }); |
| 46 | + |
| 47 | + // Assert |
| 48 | + capturedBuilder.Should().NotBeNull("builder should be passed to callback"); |
| 49 | + capturedBuilder!.Options.Should().NotBeNull("Options should be set"); |
| 50 | + capturedBuilder.Options!.Should().BeSameAs(journalOptions, "Options should reference the original options instance"); |
| 51 | + capturedBuilder.Options!.Identifier.Should().Be("test-journal"); |
| 52 | + |
| 53 | + _output.WriteLine($"✓ Journal builder correctly exposes options with identifier: {capturedBuilder.Options!.Identifier}"); |
| 54 | + } |
| 55 | + |
| 56 | + [Fact(DisplayName = "Snapshot builder should expose SnapshotOptions via Options property")] |
| 57 | + public void SnapshotBuilderShouldExposeOptions() |
| 58 | + { |
| 59 | + // Arrange |
| 60 | + var services = new ServiceCollection(); |
| 61 | + var builder = new AkkaConfigurationBuilder(services, "test-system"); |
| 62 | + var snapshotOptions = new TestSnapshotOptions(isDefault: true) |
| 63 | + { |
| 64 | + Identifier = "test-snapshot" |
| 65 | + }; |
| 66 | + |
| 67 | + AkkaPersistenceSnapshotBuilder? capturedBuilder = null; |
| 68 | + |
| 69 | + // Act |
| 70 | + builder.WithSnapshot(snapshotOptions, snapshot => |
| 71 | + { |
| 72 | + capturedBuilder = snapshot; |
| 73 | + }); |
| 74 | + |
| 75 | + // Assert |
| 76 | + capturedBuilder.Should().NotBeNull("builder should be passed to callback"); |
| 77 | + capturedBuilder!.Options.Should().NotBeNull("Options should be set"); |
| 78 | + capturedBuilder.Options!.Should().BeSameAs(snapshotOptions, "Options should reference the original options instance"); |
| 79 | + capturedBuilder.Options!.Identifier.Should().Be("test-snapshot"); |
| 80 | + |
| 81 | + _output.WriteLine($"✓ Snapshot builder correctly exposes options with identifier: {capturedBuilder.Options!.Identifier}"); |
| 82 | + } |
| 83 | + |
| 84 | + [Fact(DisplayName = "WithJournalAndSnapshot should expose both journal and snapshot options")] |
| 85 | + public void WithJournalAndSnapshotShouldExposeBothOptions() |
| 86 | + { |
| 87 | + // Arrange |
| 88 | + var services = new ServiceCollection(); |
| 89 | + var builder = new AkkaConfigurationBuilder(services, "test-system"); |
| 90 | + var journalOptions = new TestJournalOptions(isDefault: true) |
| 91 | + { |
| 92 | + Identifier = "test-journal" |
| 93 | + }; |
| 94 | + var snapshotOptions = new TestSnapshotOptions(isDefault: true) |
| 95 | + { |
| 96 | + Identifier = "test-snapshot" |
| 97 | + }; |
| 98 | + |
| 99 | + AkkaPersistenceJournalBuilder? capturedJournalBuilder = null; |
| 100 | + AkkaPersistenceSnapshotBuilder? capturedSnapshotBuilder = null; |
| 101 | + |
| 102 | + // Act |
| 103 | + builder.WithJournalAndSnapshot( |
| 104 | + journalOptions, |
| 105 | + snapshotOptions, |
| 106 | + journal => { capturedJournalBuilder = journal; }, |
| 107 | + snapshot => { capturedSnapshotBuilder = snapshot; }); |
| 108 | + |
| 109 | + // Assert |
| 110 | + capturedJournalBuilder.Should().NotBeNull("journal builder should be passed to callback"); |
| 111 | + capturedJournalBuilder!.Options.Should().NotBeNull("journal Options should be set"); |
| 112 | + capturedJournalBuilder.Options.Should().BeSameAs(journalOptions); |
| 113 | + |
| 114 | + capturedSnapshotBuilder.Should().NotBeNull("snapshot builder should be passed to callback"); |
| 115 | + capturedSnapshotBuilder!.Options.Should().NotBeNull("snapshot Options should be set"); |
| 116 | + capturedSnapshotBuilder.Options.Should().BeSameAs(snapshotOptions); |
| 117 | + |
| 118 | + _output.WriteLine($"✓ Both builders correctly expose their options"); |
| 119 | + } |
| 120 | + |
| 121 | + [Fact(DisplayName = "Legacy parameterless constructor should have null Options (backward compatibility)")] |
| 122 | + public void LegacyParameterlessConstructorShouldHaveNullOptions() |
| 123 | + { |
| 124 | + // Arrange |
| 125 | + var services = new ServiceCollection(); |
| 126 | + var akkaBuilder = new AkkaConfigurationBuilder(services, "test-system"); |
| 127 | + |
| 128 | + // Act - using the parameterless constructor directly (legacy usage) |
| 129 | + var journalBuilder = new AkkaPersistenceJournalBuilder("legacy-journal", akkaBuilder); |
| 130 | + var snapshotBuilder = new AkkaPersistenceSnapshotBuilder("legacy-snapshot", akkaBuilder); |
| 131 | + |
| 132 | + // Assert |
| 133 | + journalBuilder.Options.Should().BeNull("parameterless constructor should result in null Options for backward compatibility"); |
| 134 | + snapshotBuilder.Options.Should().BeNull("parameterless constructor should result in null Options for backward compatibility"); |
| 135 | + |
| 136 | + _output.WriteLine($"✓ Legacy constructors correctly maintain backward compatibility with null Options"); |
| 137 | + } |
| 138 | + |
| 139 | + [Fact(DisplayName = "WithInMemoryJournal should work without options (backward compatibility)")] |
| 140 | + public void WithInMemoryJournalShouldWorkWithoutOptions() |
| 141 | + { |
| 142 | + // Arrange |
| 143 | + var services = new ServiceCollection(); |
| 144 | + var builder = new AkkaConfigurationBuilder(services, "test-system"); |
| 145 | + |
| 146 | + AkkaPersistenceJournalBuilder? capturedBuilder = null; |
| 147 | + |
| 148 | + // Act |
| 149 | + builder.WithInMemoryJournal(journal => |
| 150 | + { |
| 151 | + capturedBuilder = journal; |
| 152 | + }); |
| 153 | + |
| 154 | + // Assert |
| 155 | + capturedBuilder.Should().NotBeNull("builder should be passed to callback"); |
| 156 | + capturedBuilder!.Options.Should().BeNull("WithInMemoryJournal doesn't have options, so Options should be null"); |
| 157 | + capturedBuilder.JournalId.Should().Be("inmem"); |
| 158 | + |
| 159 | + _output.WriteLine($"✓ WithInMemoryJournal correctly works without options (backward compatibility maintained)"); |
| 160 | + } |
| 161 | + |
| 162 | + /// <summary> |
| 163 | + /// Demonstrates the improved API ergonomics - extension methods can now access options |
| 164 | + /// from the builder without requiring them as explicit parameters. |
| 165 | + /// </summary> |
| 166 | + [Fact(DisplayName = "Extension methods can access connection details from builder options")] |
| 167 | + public void ExtensionMethodsCanAccessConnectionDetailsFromBuilderOptions() |
| 168 | + { |
| 169 | + // Arrange |
| 170 | + var services = new ServiceCollection(); |
| 171 | + var builder = new AkkaConfigurationBuilder(services, "test-system"); |
| 172 | + |
| 173 | + // Simulate a concrete options class with connection details |
| 174 | + var journalOptions = new TestJournalOptionsWithConnectionString(isDefault: true) |
| 175 | + { |
| 176 | + Identifier = "test-journal", |
| 177 | + ConnectionString = "Server=localhost;Database=test;User=sa;Password=pass123" |
| 178 | + }; |
| 179 | + |
| 180 | + string? capturedConnectionString = null; |
| 181 | + |
| 182 | + // Act |
| 183 | + builder.WithJournal(journalOptions, journal => |
| 184 | + { |
| 185 | + // Extension method can now access connection string from builder.Options |
| 186 | + // WITHOUT requiring it as an explicit parameter! |
| 187 | + if (journal.Options is TestJournalOptionsWithConnectionString options) |
| 188 | + { |
| 189 | + capturedConnectionString = options.ConnectionString; |
| 190 | + } |
| 191 | + }); |
| 192 | + |
| 193 | + // Assert |
| 194 | + capturedConnectionString.Should().NotBeNull("extension method should be able to access connection string from builder options"); |
| 195 | + capturedConnectionString.Should().Be("Server=localhost;Database=test;User=sa;Password=pass123"); |
| 196 | + |
| 197 | + _output.WriteLine($"✓ Extension methods can access connection details from builder without explicit parameters"); |
| 198 | + _output.WriteLine($" Connection string: {capturedConnectionString}"); |
| 199 | + } |
| 200 | + |
| 201 | + /// <summary> |
| 202 | + /// Test helper class simulating a real-world options class with connection string |
| 203 | + /// </summary> |
| 204 | + private sealed class TestJournalOptionsWithConnectionString : JournalOptions |
| 205 | + { |
| 206 | + public TestJournalOptionsWithConnectionString(bool isDefault = true) : base(isDefault) |
| 207 | + { |
| 208 | + Identifier = "test-journal-with-conn"; |
| 209 | + } |
| 210 | + |
| 211 | + public override string Identifier { get; set; } |
| 212 | + |
| 213 | + protected override Config InternalDefaultConfig => |
| 214 | + ConfigurationFactory.ParseString(""" |
| 215 | + class = "Akka.Persistence.Journal.MemoryJournal, Akka.Persistence" |
| 216 | + plugin-dispatcher = "akka.actor.default-dispatcher" |
| 217 | + """); |
| 218 | + |
| 219 | + public string? ConnectionString { get; set; } |
| 220 | + } |
| 221 | +} |
0 commit comments