using Xunit;
namespace XFEExtension.NetCore.AutoConfig.Tests;
public sealed class PersistenceTests
{
[Fact]
public void StaticPartialPropertyPreservesInitializer()
{
Assert.Equal("partial-default", PartialPropertyDefaultProfile.Value);
var separateInstance = new PartialPropertyDefaultProfile();
Assert.Equal("partial-default", separateInstance.InstanceValue);
Assert.NotSame(PartialPropertyDefaultProfile.Values, separateInstance.InstanceValues);
}
[Fact]
public async Task AllBuiltInFormatsRoundTrip()
{
var directory = CreateTestDirectory();
try
{
await AssertRoundTripAsync(
Path.Combine(directory, nameof(XfeRoundTripProfile)),
value => XfeRoundTripProfile.Value = value,
() => XfeRoundTripProfile.Value,
() => XfeRoundTripProfile.SaveProfileAsync(),
XfeRoundTripProfile.LoadProfile,
path => XfeRoundTripProfile.ProfilePath = path);
await AssertRoundTripAsync(
Path.Combine(directory, nameof(JsonRoundTripProfile)),
value => JsonRoundTripProfile.Value = value,
() => JsonRoundTripProfile.Value,
() => JsonRoundTripProfile.SaveProfileAsync(),
JsonRoundTripProfile.LoadProfile,
path => JsonRoundTripProfile.ProfilePath = path);
await AssertRoundTripAsync(
Path.Combine(directory, nameof(XmlRoundTripProfile)),
value => XmlRoundTripProfile.Value = value,
() => XmlRoundTripProfile.Value,
() => XmlRoundTripProfile.SaveProfileAsync(),
XmlRoundTripProfile.LoadProfile,
path => XmlRoundTripProfile.ProfilePath = path);
await AssertRoundTripAsync(
Path.Combine(directory, nameof(PartialPropertyProfile)),
value => PartialPropertyProfile.Value = value,
() => PartialPropertyProfile.Value,
() => PartialPropertyProfile.SaveProfileAsync(),
PartialPropertyProfile.LoadProfile,
path => PartialPropertyProfile.ProfilePath = path);
}
finally
{
DeleteDirectory(directory);
}
}
[Fact]
public void XmlUsesProfilePropertyNamesWithoutInstancePrefix()
{
XmlRoundTripProfile.Current.InstanceValue = "canonical";
var xml = XmlRoundTripProfile.ExportProfile();
PartialPropertyProfile.Value = "partial-canonical";
var partialXml = PartialPropertyProfile.ExportProfile();
Assert.Contains("<Value>canonical</Value>", xml);
Assert.DoesNotContain("<InstanceValue>", xml);
Assert.Contains("<Value>partial-canonical</Value>", partialXml);
Assert.DoesNotContain("<InstanceValue>", partialXml);
}
[Fact]
public async Task NaturalPartialPropertyRequestsAutomaticSave()
{
var directory = CreateTestDirectory();
try
{
PartialPropertyProfile.ProfilePath = Path.Combine(directory, nameof(PartialPropertyProfile));
PartialPropertyProfile.Current.AutoSaveDelay = TimeSpan.FromMilliseconds(10);
PartialPropertyProfile.Value = "auto-saved";
await PartialPropertyProfile.FlushAsync();
var xml = await File.ReadAllTextAsync(PartialPropertyProfile.ProfilePath + ".xml");
Assert.Contains("<Value>auto-saved</Value>", xml);
}
finally
{
PartialPropertyProfile.DeleteProfile();
DeleteDirectory(directory);
}
}
[Fact]
public void ProfilePathAttributeAndDefaultPathUseExpectedPaths()
{
Assert.Equal("custom/settings", AttributePathProfile.ProfilePath);
var previousPath = XFEProfile.ProfilesDefaultPath;
var root = Path.Combine(Path.GetTempPath(), "XFEAutoConfigTests", Guid.NewGuid().ToString("N"));
try
{
XFEProfile.ProfilesDefaultPath = root;
Assert.Equal(Path.Combine(root, nameof(DefaultPathProfile)), DefaultPathProfile.ProfilePath);
Assert.Equal(0, DefaultPathProfile.Value);
}
finally
{
XFEProfile.ProfilesDefaultPath = previousPath;
}
}
[Fact]
public async Task FlushPersistsPendingSaveAndHonorsCancellation()
{
var directory = CreateTestDirectory();
try
{
FlushProfile.ProfilePath = Path.Combine(directory, nameof(FlushProfile));
FlushProfile.Current.AutoSaveDelay = TimeSpan.FromSeconds(30);
FlushProfile.Value = "flushed";
await FlushProfile.FlushAsync();
var profilePath = FlushProfile.ProfilePath + ".xpf";
Assert.True(File.Exists(profilePath));
Assert.Contains("flushed", await File.ReadAllTextAsync(profilePath));
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => FlushProfile.SaveProfileAsync(new CancellationToken(true)));
}
finally
{
FlushProfile.DeleteProfile();
DeleteDirectory(directory);
}
}
[Fact]
public void CorruptProfileIsPreservedAndDefaultsRemainUsable()
{
var directory = CreateTestDirectory();
ProfileLoadFailedEventArgs? observedFailure = null;
EventHandler<ProfileLoadFailedEventArgs> handler = (_, args) => observedFailure = args;
XFEProfile.ProfileLoadFailed += handler;
try
{
CorruptJsonProfile.ProfilePath = Path.Combine(directory, nameof(CorruptJsonProfile));
var profilePath = CorruptJsonProfile.ProfilePath + ".json";
File.WriteAllText(profilePath, "{not-json");
CorruptJsonProfile.LoadProfile();
Assert.Equal("default", CorruptJsonProfile.Value);
Assert.NotNull(CorruptJsonProfile.Current.LastLoadException);
Assert.NotNull(observedFailure);
Assert.Equal(typeof(CorruptJsonProfile), observedFailure!.ProfileType);
Assert.False(File.Exists(profilePath));
Assert.NotNull(observedFailure.BackupPath);
Assert.True(File.Exists(observedFailure.BackupPath));
}
finally
{
XFEProfile.ProfileLoadFailed -= handler;
DeleteDirectory(directory);
}
}
[Fact]
public async Task SaveFailureIsExposedAndPathChangesTakeEffect()
{
var directory = CreateTestDirectory();
try
{
var blockingFile = Path.Combine(directory, "not-a-directory");
await File.WriteAllTextAsync(blockingFile, "block");
FailingSaveProfile.ProfilePath = Path.Combine(blockingFile, "profile");
FailingSaveProfile.Value = "cannot-save";
await Assert.ThrowsAnyAsync<IOException>(() => FailingSaveProfile.SaveProfileAsync());
Assert.IsType<IOException>(FailingSaveProfile.Current.LastSaveException, exactMatch: false);
var firstPath = Path.Combine(directory, "first", nameof(PathChangeProfile));
var secondPath = Path.Combine(directory, "second", nameof(PathChangeProfile));
PathChangeProfile.ProfilePath = firstPath;
PathChangeProfile.Value = 1;
await PathChangeProfile.SaveProfileAsync();
PathChangeProfile.ProfilePath = secondPath;
PathChangeProfile.Value = 2;
await PathChangeProfile.SaveProfileAsync();
Assert.True(File.Exists(firstPath + ".xpf"));
Assert.True(File.Exists(secondPath + ".xpf"));
}
finally
{
DeleteDirectory(directory);
}
}
[Fact]
public async Task UnixPermissionDenialIsSurfaced()
{
if (OperatingSystem.IsWindows())
return;
var directory = CreateTestDirectory();
try
{
FailingSaveProfile.ProfilePath = Path.Combine(directory, nameof(FailingSaveProfile));
File.SetUnixFileMode(directory, UnixFileMode.UserRead | UnixFileMode.UserExecute);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => FailingSaveProfile.SaveProfileAsync());
Assert.IsType<UnauthorizedAccessException>(FailingSaveProfile.Current.LastSaveException);
}
finally
{
File.SetUnixFileMode(directory, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
DeleteDirectory(directory);
}
}
private static async Task AssertRoundTripAsync(string path, Action<string> setValue, Func<string> getValue, Func<Task> save, Action load, Action<string> setPath)
{
setPath(path);
setValue("persisted");
await save();
setValue("changed");
load();
Assert.Equal("persisted", getValue());
}
private static string CreateTestDirectory()
{
var directory = Path.Combine(Path.GetTempPath(), "XFEAutoConfigTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
return directory;
}
private static void DeleteDirectory(string directory)
{
if (Directory.Exists(directory))
Directory.Delete(directory, true);
}
}
using Xunit;
namespace XFEExtension.NetCore.AutoConfig.Tests;
public sealed class PersistenceTests
{
[Fact]
public void StaticPartialPropertyPreservesInitializer()
{
Assert.Equal("partial-default", PartialPropertyDefaultProfile.Value);
var separateInstance = new PartialPropertyDefaultProfile();
Assert.Equal("partial-default", separateInstance.InstanceValue);
Assert.NotSame(PartialPropertyDefaultProfile.Values, separateInstance.InstanceValues);
}
[Fact]
public async Task AllBuiltInFormatsRoundTrip()
{
var directory = CreateTestDirectory();
try
{
await AssertRoundTripAsync(
Path.Combine(directory, nameof(XfeRoundTripProfile)),
value => XfeRoundTripProfile.Value = value,
() => XfeRoundTripProfile.Value,
() => XfeRoundTripProfile.SaveProfileAsync(),
XfeRoundTripProfile.LoadProfile,
path => XfeRoundTripProfile.ProfilePath = path);
await AssertRoundTripAsync(
Path.Combine(directory, nameof(JsonRoundTripProfile)),
value => JsonRoundTripProfile.Value = value,
() => JsonRoundTripProfile.Value,
() => JsonRoundTripProfile.SaveProfileAsync(),
JsonRoundTripProfile.LoadProfile,
path => JsonRoundTripProfile.ProfilePath = path);
await AssertRoundTripAsync(
Path.Combine(directory, nameof(XmlRoundTripProfile)),
value => XmlRoundTripProfile.Value = value,
() => XmlRoundTripProfile.Value,
() => XmlRoundTripProfile.SaveProfileAsync(),
XmlRoundTripProfile.LoadProfile,
path => XmlRoundTripProfile.ProfilePath = path);
await AssertRoundTripAsync(
Path.Combine(directory, nameof(PartialPropertyProfile)),
value => PartialPropertyProfile.Value = value,
() => PartialPropertyProfile.Value,
() => PartialPropertyProfile.SaveProfileAsync(),
PartialPropertyProfile.LoadProfile,
path => PartialPropertyProfile.ProfilePath = path);
}
finally
{
DeleteDirectory(directory);
}
}
[Fact]
public void XmlUsesProfilePropertyNamesWithoutInstancePrefix()
{
XmlRoundTripProfile.Current.InstanceValue = "canonical";
var xml = XmlRoundTripProfile.ExportProfile();
PartialPropertyProfile.Value = "partial-canonical";
var partialXml = PartialPropertyProfile.ExportProfile();
Assert.Contains("<Value>canonical</Value>", xml);
Assert.DoesNotContain("<InstanceValue>", xml);
Assert.Contains("<Value>partial-canonical</Value>", partialXml);
Assert.DoesNotContain("<InstanceValue>", partialXml);
}
[Fact]
public async Task NaturalPartialPropertyRequestsAutomaticSave()
{
var directory = CreateTestDirectory();
try
{
PartialPropertyProfile.ProfilePath = Path.Combine(directory, nameof(PartialPropertyProfile));
PartialPropertyProfile.Current.AutoSaveDelay = TimeSpan.FromMilliseconds(10);
PartialPropertyProfile.Value = "auto-saved";
await PartialPropertyProfile.FlushAsync();
var xml = await File.ReadAllTextAsync(PartialPropertyProfile.ProfilePath + ".xml");
Assert.Contains("<Value>auto-saved</Value>", xml);
}
finally
{
PartialPropertyProfile.DeleteProfile();
DeleteDirectory(directory);
}
}
[Fact]
public void ProfilePathAttributeAndDefaultPathUseExpectedPaths()
{
Assert.Equal("custom/settings", AttributePathProfile.ProfilePath);
var previousPath = XFEProfile.ProfilesDefaultPath;
var root = Path.Combine(Path.GetTempPath(), "XFEAutoConfigTests", Guid.NewGuid().ToString("N"));
try
{
XFEProfile.ProfilesDefaultPath = root;
Assert.Equal(Path.Combine(root, nameof(DefaultPathProfile)), DefaultPathProfile.ProfilePath);
Assert.Equal(0, DefaultPathProfile.Value);
}
finally
{
XFEProfile.ProfilesDefaultPath = previousPath;
}
}
[Fact]
public async Task FlushPersistsPendingSaveAndHonorsCancellation()
{
var directory = CreateTestDirectory();
try
{
FlushProfile.ProfilePath = Path.Combine(directory, nameof(FlushProfile));
FlushProfile.Current.AutoSaveDelay = TimeSpan.FromSeconds(30);
FlushProfile.Value = "flushed";
await FlushProfile.FlushAsync();
var profilePath = FlushProfile.ProfilePath + ".xpf";
Assert.True(File.Exists(profilePath));
Assert.Contains("flushed", await File.ReadAllTextAsync(profilePath));
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => FlushProfile.SaveProfileAsync(new CancellationToken(true)));
}
finally
{
FlushProfile.DeleteProfile();
DeleteDirectory(directory);
}
}
[Fact]
public void CorruptProfileIsPreservedAndDefaultsRemainUsable()
{
var directory = CreateTestDirectory();
ProfileLoadFailedEventArgs? observedFailure = null;
EventHandler<ProfileLoadFailedEventArgs> handler = (_, args) => observedFailure = args;
XFEProfile.ProfileLoadFailed += handler;
try
{
CorruptJsonProfile.ProfilePath = Path.Combine(directory, nameof(CorruptJsonProfile));
var profilePath = CorruptJsonProfile.ProfilePath + ".json";
File.WriteAllText(profilePath, "{not-json");
CorruptJsonProfile.LoadProfile();
Assert.Equal("default", CorruptJsonProfile.Value);
Assert.NotNull(CorruptJsonProfile.Current.LastLoadException);
Assert.NotNull(observedFailure);
Assert.Equal(typeof(CorruptJsonProfile), observedFailure!.ProfileType);
Assert.False(File.Exists(profilePath));
Assert.NotNull(observedFailure.BackupPath);
Assert.True(File.Exists(observedFailure.BackupPath));
}
finally
{
XFEProfile.ProfileLoadFailed -= handler;
DeleteDirectory(directory);
}
}
[Fact]
public async Task SaveFailureIsExposedAndPathChangesTakeEffect()
{
var directory = CreateTestDirectory();
try
{
var blockingFile = Path.Combine(directory, "not-a-directory");
await File.WriteAllTextAsync(blockingFile, "block");
FailingSaveProfile.ProfilePath = Path.Combine(blockingFile, "profile");
FailingSaveProfile.Value = "cannot-save";
await Assert.ThrowsAnyAsync<IOException>(() => FailingSaveProfile.SaveProfileAsync());
Assert.IsType<IOException>(FailingSaveProfile.Current.LastSaveException, exactMatch: false);
var firstPath = Path.Combine(directory, "first", nameof(PathChangeProfile));
var secondPath = Path.Combine(directory, "second", nameof(PathChangeProfile));
PathChangeProfile.ProfilePath = firstPath;
PathChangeProfile.Value = 1;
await PathChangeProfile.SaveProfileAsync();
PathChangeProfile.ProfilePath = secondPath;
PathChangeProfile.Value = 2;
await PathChangeProfile.SaveProfileAsync();
Assert.True(File.Exists(firstPath + ".xpf"));
Assert.True(File.Exists(secondPath + ".xpf"));
}
finally
{
DeleteDirectory(directory);
}
}
[Fact]
public async Task UnixPermissionDenialIsSurfaced()
{
if (OperatingSystem.IsWindows())
return;
var directory = CreateTestDirectory();
try
{
FailingSaveProfile.ProfilePath = Path.Combine(directory, nameof(FailingSaveProfile));
File.SetUnixFileMode(directory, UnixFileMode.UserRead | UnixFileMode.UserExecute);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => FailingSaveProfile.SaveProfileAsync());
Assert.IsType<UnauthorizedAccessException>(FailingSaveProfile.Current.LastSaveException);
}
finally
{
File.SetUnixFileMode(directory, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
DeleteDirectory(directory);
}
}
private static async Task AssertRoundTripAsync(string path, Action<string> setValue, Func<string> getValue, Func<Task> save, Action load, Action<string> setPath)
{
setPath(path);
setValue("persisted");
await save();
setValue("changed");
load();
Assert.Equal("persisted", getValue());
}
private static string CreateTestDirectory()
{
var directory = Path.Combine(Path.GetTempPath(), "XFEAutoConfigTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
return directory;
}
private static void DeleteDirectory(string directory)
{
if (Directory.Exists(directory))
Directory.Delete(directory, true);
}
}