diff --git a/AndreGoepel.Marten.Configuration.slnx b/AndreGoepel.Marten.Configuration.slnx index d9ca280..84fb1d3 100644 --- a/AndreGoepel.Marten.Configuration.slnx +++ b/AndreGoepel.Marten.Configuration.slnx @@ -9,6 +9,7 @@ + diff --git a/CLAUDE.md b/CLAUDE.md index 34e40d3..52bf750 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,16 +27,25 @@ among others. Plain class library — no ASP.NET Core dependency. - `SaveAsync` sets `SettingsDocument.Id` from `T.DocumentId` itself — callers never set it manually. - Public API surface is only `SettingsDocument`, `ISettingsDocument`, - `ISettingsStore`, `SettingsStoreOptionsExtensions`, `Initialization` — - everything else stays `internal`. + `ISettingsStore`, `SettingsStoreOptionsExtensions`, `Initialization`, + `DataProtectorExtensions` — everything else stays `internal`. +- `DataProtectorExtensions.ProtectOrKeepExisting` is the shared + "protect a new value, or keep the existing ciphertext when the caller + left the field blank" round trip for a secret field inside a settings + document (SMTP password, API token, provider credential, ...). It takes + the caller's own `IDataProtector` — this library never constructs one + itself, so it only depends on `Microsoft.AspNetCore.DataProtection.Abstractions` + (interfaces only), not the full ASP.NET Core shared framework. - Exception: `MartenSettingsStore` is `public` (not `internal`) despite the rule above — Wolverine's `NotAllowed` service-location policy constructs it directly in generated handler code and requires public visibility. ## Testing -- Scope: the Marten store and schema-hierarchy behavior (round-trip, - shared-table mapping); integration tests need Docker for the Postgres - container +- `AndreGoepel.Marten.Configuration.Tests` — pure unit tests, no I/O + (e.g. `DataProtectorExtensions`) +- `AndreGoepel.Marten.Configuration.IntegrationTests` — the Marten store + and schema-hierarchy behavior (round-trip, shared-table mapping); needs + Docker for the Postgres container ## Conventions Audit - `.editorconfig` is intentionally the 26-line variant (no Razor/CSS/JS diff --git a/Directory.Packages.props b/Directory.Packages.props index 85c0160..355f83a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,6 +15,10 @@ /> + + + + diff --git a/src/AndreGoepel.Marten.Configuration/AndreGoepel.Marten.Configuration.csproj b/src/AndreGoepel.Marten.Configuration/AndreGoepel.Marten.Configuration.csproj index f9aa8b7..d70edd8 100644 --- a/src/AndreGoepel.Marten.Configuration/AndreGoepel.Marten.Configuration.csproj +++ b/src/AndreGoepel.Marten.Configuration/AndreGoepel.Marten.Configuration.csproj @@ -20,10 +20,12 @@ + + diff --git a/src/AndreGoepel.Marten.Configuration/DataProtectorExtensions.cs b/src/AndreGoepel.Marten.Configuration/DataProtectorExtensions.cs new file mode 100644 index 0000000..121f098 --- /dev/null +++ b/src/AndreGoepel.Marten.Configuration/DataProtectorExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.DataProtection; + +namespace AndreGoepel.Marten.Configuration; + +/// +/// Shared round trip for a DataProtection-encrypted secret field inside a +/// (an SMTP password, an API token, a provider credential, ...). +/// +public static class DataProtectorExtensions +{ + /// + /// Protects when the caller supplied one; otherwise keeps + /// unchanged. This is the "leave the field blank to + /// keep the current secret" idiom used by every settings form that edits a protected value. + /// Returns null when there is neither a new value nor an existing one — callers + /// decide what that means for them (reject the save, or protect an explicit empty string + /// when a blank secret is itself a valid saved state). + /// + public static string? ProtectOrKeepExisting( + this IDataProtector protector, + string? newPlaintext, + string? existingCiphertext + ) + { + ArgumentNullException.ThrowIfNull(protector); + return !string.IsNullOrEmpty(newPlaintext) + ? protector.Protect(newPlaintext) + : existingCiphertext; + } +} diff --git a/src/AndreGoepel.Marten.Configuration/packages.lock.json b/src/AndreGoepel.Marten.Configuration/packages.lock.json index 993a654..9057e77 100644 --- a/src/AndreGoepel.Marten.Configuration/packages.lock.json +++ b/src/AndreGoepel.Marten.Configuration/packages.lock.json @@ -16,6 +16,12 @@ "Weasel.Storage": "9.17.0" } }, + "Microsoft.AspNetCore.DataProtection.Abstractions": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "q2RNx0N/qrsU+JgbPE78I9pu5ekwguitAFVoeE1NCgxtkUTguvf7oTzBnn42zSQxuugJ4fmj0RSarob8Rvorrg==" + }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Direct", "requested": "[10.0.10, )", diff --git a/tests/AndreGoepel.Marten.Configuration.IntegrationTests/packages.lock.json b/tests/AndreGoepel.Marten.Configuration.IntegrationTests/packages.lock.json index e6f5995..1c7f7e9 100644 --- a/tests/AndreGoepel.Marten.Configuration.IntegrationTests/packages.lock.json +++ b/tests/AndreGoepel.Marten.Configuration.IntegrationTests/packages.lock.json @@ -693,6 +693,7 @@ "type": "Project", "dependencies": { "Marten": "[9.19.0, )", + "Microsoft.AspNetCore.DataProtection.Abstractions": "[10.0.10, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.10, )" } }, @@ -710,6 +711,12 @@ "Weasel.Storage": "9.17.0" } }, + "Microsoft.AspNetCore.DataProtection.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "q2RNx0N/qrsU+JgbPE78I9pu5ekwguitAFVoeE1NCgxtkUTguvf7oTzBnn42zSQxuugJ4fmj0RSarob8Rvorrg==" + }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "CentralTransitive", "requested": "[10.0.10, )", diff --git a/tests/AndreGoepel.Marten.Configuration.Tests/AndreGoepel.Marten.Configuration.Tests.csproj b/tests/AndreGoepel.Marten.Configuration.Tests/AndreGoepel.Marten.Configuration.Tests.csproj new file mode 100644 index 0000000..271292b --- /dev/null +++ b/tests/AndreGoepel.Marten.Configuration.Tests/AndreGoepel.Marten.Configuration.Tests.csproj @@ -0,0 +1,32 @@ + + + net10.0 + enable + enable + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/tests/AndreGoepel.Marten.Configuration.Tests/DataProtectorExtensionsTests.cs b/tests/AndreGoepel.Marten.Configuration.Tests/DataProtectorExtensionsTests.cs new file mode 100644 index 0000000..d5bd07e --- /dev/null +++ b/tests/AndreGoepel.Marten.Configuration.Tests/DataProtectorExtensionsTests.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.DataProtection; + +namespace AndreGoepel.Marten.Configuration.Tests; + +public sealed class DataProtectorExtensionsTests +{ + private readonly IDataProtector protector = + new EphemeralDataProtectionProvider().CreateProtector( + "AndreGoepel.Marten.Configuration.Tests.DataProtectorExtensions" + ); + + [Fact] + public void ProtectOrKeepExisting_NewPlaintextGiven_ProtectsTheNewValue() + { + // Act + var ciphertext = protector.ProtectOrKeepExisting("new-secret", "old-ciphertext"); + + // Assert + Assert.NotNull(ciphertext); + Assert.NotEqual("old-ciphertext", ciphertext); + Assert.Equal("new-secret", protector.Unprotect(ciphertext)); + } + + [Fact] + public void ProtectOrKeepExisting_NewPlaintextEmpty_KeepsExistingCiphertext() + { + // Act + var ciphertext = protector.ProtectOrKeepExisting(string.Empty, "old-ciphertext"); + + // Assert + Assert.Equal("old-ciphertext", ciphertext); + } + + [Fact] + public void ProtectOrKeepExisting_NewPlaintextNull_KeepsExistingCiphertext() + { + // Act + var ciphertext = protector.ProtectOrKeepExisting(null, "old-ciphertext"); + + // Assert + Assert.Equal("old-ciphertext", ciphertext); + } + + [Fact] + public void ProtectOrKeepExisting_NeitherNewValueNorExistingCiphertextGiven_ReturnsNull() + { + // Act + var ciphertext = protector.ProtectOrKeepExisting(null, null); + + // Assert + Assert.Null(ciphertext); + } + + [Fact] + public void ProtectOrKeepExisting_RoundTripsThroughUnprotect() + { + // Act + var ciphertext = protector.ProtectOrKeepExisting("correct horse battery staple", null); + + // Assert + Assert.NotNull(ciphertext); + Assert.Equal("correct horse battery staple", protector.Unprotect(ciphertext)); + } + + [Fact] + public void ProtectOrKeepExisting_NullProtector_Throws() + { + // Act / Assert + Assert.Throws(() => + DataProtectorExtensions.ProtectOrKeepExisting(null!, "value", null) + ); + } +} diff --git a/tests/AndreGoepel.Marten.Configuration.Tests/GlobalUsings.cs b/tests/AndreGoepel.Marten.Configuration.Tests/GlobalUsings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/tests/AndreGoepel.Marten.Configuration.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/tests/AndreGoepel.Marten.Configuration.Tests/packages.lock.json b/tests/AndreGoepel.Marten.Configuration.Tests/packages.lock.json new file mode 100644 index 0000000..57b9a2c --- /dev/null +++ b/tests/AndreGoepel.Marten.Configuration.Tests/packages.lock.json @@ -0,0 +1,316 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "27jXSV/0DbVqF5jDrAxuQFZ9oaz6gmG03p8ttxAFk+X0M4woFYj7MoWDLCna5EGLb0CE6OE7X6ZH3Wt5smTtaA==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[18.8.1, )", + "resolved": "18.8.1", + "contentHash": "dknJL3/9Y3t4XuCBqnc0PevPxgLsUMmVhjwup/b1HNovA8zWcj3XsfIf7c6p05363DWcqL7X/YhDL9B+Zymv1w==", + "dependencies": { + "Microsoft.CodeCoverage": "18.8.1", + "Microsoft.TestPlatform.TestHost": "18.8.1" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" + }, + "xunit.v3": { + "type": "Direct", + "requested": "[3.2.2, )", + "resolved": "3.2.2", + "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", + "dependencies": { + "xunit.v3.mtp-v1": "[3.2.2]" + } + }, + "DistributedLock.Core": { + "type": "Transitive", + "resolved": "1.0.8", + "contentHash": "LAOsY8WxX8JU/n3lfXFz+f2pfnv0+4bHkCrOO3bwa28u9HrS3DlxSG6jf+u76SqesKs+KehZi0CndkfaUXBKvg==" + }, + "DistributedLock.Postgres": { + "type": "Transitive", + "resolved": "1.3.0", + "contentHash": "CyjXmbhFgG30qPg4DwGnsmZO63y5DBRo+PI2xW0NwtFrsYsMVt/T1RcEUlb36JMKhDkf+euhnkanv/nU+W35qA==", + "dependencies": { + "DistributedLock.Core": "[1.0.8, 1.1.0)", + "Npgsql": "8.0.6" + } + }, + "FastExpressionCompiler": { + "type": "Transitive", + "resolved": "5.4.1", + "contentHash": "nqMykMspK5cQd35Y8ulQzAujxCcCwZVUne32pC5xNpXqrbPCuKOPMWkKHJ3LveAZPm6dsVvump0TJ1SZ9RzfTQ==" + }, + "ImTools": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "dms0JfLKC9AQbQX/EdpRjn59EjEvaCxIgv1z9YxbEQb5SiOVoo7vlIKdEySeEwUkWhN05rZnNpUpG+aw5VqPVQ==" + }, + "JasperFx": { + "type": "Transitive", + "resolved": "2.35.0", + "contentHash": "+UzKoGkAtfuxfAIPvGjtfjF8gV7mjLE1bfbPPkcHvuVEeemFJtTuXYiR2qxJCO4TiBTxRzuWG6CmIy9PoaKh8Q==", + "dependencies": { + "FastExpressionCompiler": "5.4.1", + "ImTools": "4.0.0", + "Microsoft.Bcl.TimeProvider": "10.0.0", + "Polly.Core": "8.6.5", + "Spectre.Console": "0.55.0" + } + }, + "JasperFx.Events": { + "type": "Transitive", + "resolved": "2.35.0", + "contentHash": "L1HYGPOwsSne/B4nEeUu4eM0VFWgH6WFNbUc6aol4bfknbxl6NRhpvA25vXKgWCG4dfqeUSjUewjI26RkC31tw==", + "dependencies": { + "JasperFx": "2.35.0" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.Bcl.TimeProvider": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "bUubrBD6tRJE3V1kvRloYc6NymH3R5oFKjAS9e0ELNx6u0ZR+zjps9dDQyjgqN/rArzl7f+21KGszj3YRN7F2Q==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.8.1", + "contentHash": "Eclse/ZZjr4lmWzZFNN9h/OluhKL+SK/QbUyKUewgX139aGeyMEO/DkMPwuFs2MixvanTnz6891rF8UHDg+W4Q==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "Transitive", + "resolved": "3.0.1", + "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==" + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", + "dependencies": { + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", + "dependencies": { + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.8.1", + "contentHash": "qLbktNB1+b1XZLNJBTzaWVVJAd6PEzD7cgD406geMb6PcFZhp3EDNa1tctWx1+mtMU6MP/6ozVvFPC9vs2a9rw==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.8.1", + "contentHash": "FaQHPDTUOcE+SFTjssNPfrub2lT9Zyon4J2W/KLHt/efLJACb1TCeWXyOgh0D/4Q1e4n+S3E6mOKud+9nLZlEA==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.8.1" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "NetTopologySuite": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==" + }, + "NetTopologySuite.IO.PostGis": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "3W8XTFz8iP6GQ5jDXK1/LANHiU+988k1kmmuPWNKcJLpmSg6CvFpbTpz+s4+LBzkAp64wHGOldSlkSuzYfrIKA==", + "dependencies": { + "NetTopologySuite": "[2.0.0, 3.0.0-A)" + } + }, + "Npgsql": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "68BASXH0FAEuL/J4J0eRfYC8/3vzqQTmoW8zDzNf0JgaVxc7LZeEkS6jaG0ib3voFLxY5ZiCwJG+uQM+mzuu0Q==" + }, + "Npgsql.NetTopologySuite": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "7AwBLPz8EPmgAXveZ55K0Tz/9bNb8j4VI4pkTOQjyHrce8wWfAA9pefm3REidy+Kt2UFiAWef34YVi7sb/kB4A==", + "dependencies": { + "NetTopologySuite": "2.5.0", + "NetTopologySuite.IO.PostGIS": "2.1.0", + "Npgsql": "9.0.4" + } + }, + "Polly.Core": { + "type": "Transitive", + "resolved": "8.6.5", + "contentHash": "t+sUVrIwvo7UmsgHGgOG9F0GDZSRIm47u2ylH17Gvcv1q5hNEwgD5GoBlFyc0kh/pebmPyrAgvGsR/65ZBaXlg==" + }, + "Spectre.Console": { + "type": "Transitive", + "resolved": "0.55.0", + "contentHash": "2TVhUHFsDux0Z+y2Hv4bFsOMuON4SuotEEKkMparFkp5Rm259naaaEQrN4Sv6C1cbsiGKPjfkJDGgUp+hZPjTw==", + "dependencies": { + "Spectre.Console.Ansi": "0.55.0" + } + }, + "Spectre.Console.Ansi": { + "type": "Transitive", + "resolved": "0.55.0", + "contentHash": "GIfgOvuF8MpU52bprhwtDEtx0gmVIGOepM8bw0lH/TSMD0rg1Xp+5Y53kPG8rFVdcpbNANlhDQ5mEVVPO3/2Tg==" + }, + "Weasel.Core": { + "type": "Transitive", + "resolved": "9.17.0", + "contentHash": "KkIlUx6QS9i1+yk2klpUXeLFaXNWP77+CHQWPnx1wn3QKCz78PNAymI+Lsk/q35Rajzhp+mH7yr7aJrayYttsw==", + "dependencies": { + "JasperFx": "2.24.1" + } + }, + "Weasel.Postgresql": { + "type": "Transitive", + "resolved": "9.17.0", + "contentHash": "gnRO4Gh280a3OURLHC1uaJZ7eTCDR3ECIpzOJBlUq5vtzHdRFVoZdwQQLxh2WhA1dKeFYNxMeYcMeluG2qDAVg==", + "dependencies": { + "DistributedLock.Postgres": "1.3.0", + "JasperFx.Events": "2.0.0", + "Npgsql": "9.0.4", + "Npgsql.NetTopologySuite": "9.0.4", + "Weasel.Core": "9.17.0" + } + }, + "Weasel.Storage": { + "type": "Transitive", + "resolved": "9.17.0", + "contentHash": "r7s13H1xMDx9JK/MiqTRKzZ3rkh2kx9cQMGsoQ2kBsOFaA4F8CMpmkKW+9rC13OAVIxJroaJuGYU0kGMvpFPcg==", + "dependencies": { + "JasperFx.Events": "2.0.0", + "Weasel.Core": "9.17.0" + } + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.27.0", + "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v1": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "1.9.1", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", + "Microsoft.Testing.Platform": "1.9.1", + "Microsoft.Testing.Platform.MSBuild": "1.9.1", + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.inproc.console": "[3.2.2]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", + "dependencies": { + "xunit.v3.common": "[3.2.2]" + } + }, + "xunit.v3.mtp-v1": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", + "dependencies": { + "xunit.analyzers": "1.27.0", + "xunit.v3.assert": "[3.2.2]", + "xunit.v3.core.mtp-v1": "[3.2.2]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "xunit.v3.common": "[3.2.2]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", + "dependencies": { + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.common": "[3.2.2]" + } + }, + "andregoepel.marten.configuration": { + "type": "Project", + "dependencies": { + "Marten": "[9.19.0, )" + } + }, + "Marten": { + "type": "CentralTransitive", + "requested": "[9.19.0, )", + "resolved": "9.19.0", + "contentHash": "4QLFDZ53Rv6pqc25G2ay210H+G5aqbBn/+aMC/t6XBpU5aE1LORKH1RBh8a4SLt0ZZrGBz839W8UQamjBt3tNA==", + "dependencies": { + "JasperFx": "2.35.0", + "JasperFx.Events": "2.35.0", + "Microsoft.IO.RecyclableMemoryStream": "3.0.1", + "Weasel.Postgresql": "9.17.0", + "Weasel.Storage": "9.17.0" + } + } + } + } +} \ No newline at end of file