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("canonical", xml); Assert.DoesNotContain("", xml); Assert.Contains("partial-canonical", partialXml); Assert.DoesNotContain("", 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("auto-saved", 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(() => FlushProfile.SaveProfileAsync(new CancellationToken(true))); } finally { FlushProfile.DeleteProfile(); DeleteDirectory(directory); } } [Fact] public void CorruptProfileIsPreservedAndDefaultsRemainUsable() { var directory = CreateTestDirectory(); ProfileLoadFailedEventArgs? observedFailure = null; EventHandler 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(() => FailingSaveProfile.SaveProfileAsync()); Assert.IsType(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(() => FailingSaveProfile.SaveProfileAsync()); Assert.IsType(FailingSaveProfile.Current.LastSaveException); } finally { File.SetUnixFileMode(directory, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); DeleteDirectory(directory); } } private static async Task AssertRoundTripAsync(string path, Action setValue, Func getValue, Func save, Action load, Action 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); } }