diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 59d5e412c..9f0e8f76b 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -148,7 +148,7 @@ private void ConfigureRootView(MainView rootView) public static async Task PushBlogsPageAsync() { var app = (App)Current!; - var mainVm = app.ServiceProvider!.GetRequiredService(); + var mainVm = app.ServiceProvider!.GetRequiredService(); await mainVm.InitializeAsync(); await app.PushPageAsync(mainVm); } diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs index c0831b290..c87eb9286 100644 --- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using PostIt.Services; using PostIt.ViewModels; using PostIt.Views; +using PostIt.Views.Blogs; using PostIt.Views.Commands; using Yavsc.Api.Client; @@ -34,7 +35,7 @@ public static class ServiceCollectionHelpers // Vues services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); // SettingsPage is a singleton: there must be one and only one @@ -89,7 +90,7 @@ public static class ServiceCollectionHelpers sessionStatus.Refresh(); services.AddSingleton(sessionStatus); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); return services.BuildServiceProvider(); } } diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs index f866e1bac..97538abbb 100644 --- a/src/PostIt/PostIt/ViewLocator.cs +++ b/src/PostIt/PostIt/ViewLocator.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using PostIt.ViewModels; using PostIt.ViewModels.Commands; using PostIt.Views; +using PostIt.Views.Blogs; using PostIt.Views.Commands; namespace PostIt; @@ -38,7 +39,7 @@ public class ViewLocator : IDataTemplate var services = app!.ServiceProvider!; return data switch { - MainViewModel => services.GetRequiredService(), + BlogsViewModel => services.GetRequiredService(), Settings => services.GetRequiredService(), HomePageViewModel => services.GetRequiredService(), ActivitiesPageViewModel => services.GetRequiredService(), diff --git a/src/PostIt/PostIt/ViewModels/Layout/MainViewModel.cs b/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs similarity index 98% rename from src/PostIt/PostIt/ViewModels/Layout/MainViewModel.cs rename to src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs index d2667c012..94a143aa4 100644 --- a/src/PostIt/PostIt/ViewModels/Layout/MainViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs @@ -12,7 +12,7 @@ using PostIt.Helpers; namespace PostIt.ViewModels; -public partial class MainViewModel : ViewModelBase, IActionStatusViewModel +public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel { /// Window/tab title. Cosmetic — bound by /// MainPage.axaml if at all. Not the post title. @@ -336,7 +336,7 @@ public partial class MainViewModel : ViewModelBase, IActionStatusViewModel } - public MainViewModel() + public BlogsViewModel() { SettingsModel = new Settings(); Init(SettingsModel); @@ -396,7 +396,7 @@ public partial class MainViewModel : ViewModelBase, IActionStatusViewModel /// . Production code uses the /// (Settings, BlogApiClient) overload below. /// - public MainViewModel(BlogApiClient blogClient, Settings? settings = null, IServiceProvider? services = null) + public BlogsViewModel(BlogApiClient blogClient, Settings? settings = null, IServiceProvider? services = null) { SettingsModel = new Settings(); BlogClient = blogClient ?? throw new ArgumentNullException(nameof(blogClient)); ; diff --git a/src/PostIt/PostIt/Views/Blogs/MainPage.axaml b/src/PostIt/PostIt/Views/Blogs/MainPage.axaml index 871a2f541..c93aef2c2 100644 --- a/src/PostIt/PostIt/Views/Blogs/MainPage.axaml +++ b/src/PostIt/PostIt/Views/Blogs/MainPage.axaml @@ -5,15 +5,15 @@ xmlns:vm="using:PostIt.ViewModels" xmlns:postitControls="using:PostIt.Controls" xmlns:models="using:Yavsc.Blogspot" - xmlns:views="using:PostIt.Views" + xmlns:views="using:PostIt.Views.Blogs" xmlns:AvaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit" mc:Ignorable="d" - x:Class="PostIt.Views.MainPage" - x:DataType="vm:MainViewModel" + x:Class="PostIt.Views.Blogs.BlogsPage" + x:DataType="vm:BlogsViewModel" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + + x:DataType="vm:HomePageViewModel"> - + diff --git a/src/PostIt/PostIt/Views/Layout/MainView.axaml.cs b/src/PostIt/PostIt/Views/Layout/MainView.axaml.cs index 654b90e2d..22bf537c6 100644 --- a/src/PostIt/PostIt/Views/Layout/MainView.axaml.cs +++ b/src/PostIt/PostIt/Views/Layout/MainView.axaml.cs @@ -13,7 +13,7 @@ public partial class MainView : UserControl protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); - if (DataContext is ViewModels.MainViewModel vm) + if (DataContext is ViewModels.BlogsViewModel vm) { if (!vm.IsLoaded) { diff --git a/src/Yavsc.Org.Tests/Directory.Packages.props b/src/Yavsc.Org.Tests/Directory.Packages.props index 5927d2b5b..af2ef948c 100644 --- a/src/Yavsc.Org.Tests/Directory.Packages.props +++ b/src/Yavsc.Org.Tests/Directory.Packages.props @@ -4,8 +4,8 @@ - - - + + + diff --git a/src/Yavsc.Org.Tests/Services/FileSystemAuthManagerTests.cs b/src/Yavsc.Org.Tests/Services/FileSystemAuthManagerTests.cs new file mode 100644 index 000000000..8a796e4b5 --- /dev/null +++ b/src/Yavsc.Org.Tests/Services/FileSystemAuthManagerTests.cs @@ -0,0 +1,129 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Yavsc.Models; +using Yavsc.Models.Access; +using Yavsc.Models.Relationship; +using Yavsc.Services; + +namespace Yavsc.Org.Tests.Services; + +public class FileSystemAuthManagerTests +{ + [Fact] + public void SetAccess_creates_acl_row_with_owner_path_and_flags() + { + using var scope = CreateScope(); + scope.Service.SetAccess(scope.Circle.Id, "alice/documents/report.txt", FileAccessRight.Read | FileAccessRight.Write); + + var row = scope.Db.CircleAuthorizationToFile.Single(); + + Assert.Equal(scope.Circle.Id, row.CircleId); + Assert.Equal("alice/documents/report.txt", row.Path); + Assert.Equal("alice", row.OwnerId); + Assert.Equal(FileAccessRight.Read | FileAccessRight.Write, row.Access); + } + + [Fact] + public void SetAccess_updates_existing_acl_row_without_duplicates() + { + using var scope = CreateScope(); + + scope.Service.SetAccess(scope.Circle.Id, "alice/documents/report.txt", FileAccessRight.Read); + scope.Service.SetAccess(scope.Circle.Id, "alice/documents/report.txt", FileAccessRight.Write); + + var rows = scope.Db.CircleAuthorizationToFile.ToList(); + + Assert.Single(rows); + Assert.Equal(FileAccessRight.Write, rows[0].Access); + } + + [Fact] + public void SetAccess_none_removes_existing_acl_row() + { + using var scope = CreateScope(); + + scope.Service.SetAccess(scope.Circle.Id, "alice/documents/report.txt", FileAccessRight.Read); + scope.Service.SetAccess(scope.Circle.Id, "alice/documents/report.txt", FileAccessRight.None); + + Assert.Empty(scope.Db.CircleAuthorizationToFile); + } + + [Fact] + public void SetAccess_ignores_unknown_owner_prefix() + { + using var scope = CreateScope(); + + scope.Service.SetAccess(scope.Circle.Id, "unknown/documents/report.txt", FileAccessRight.Read); + + Assert.Empty(scope.Db.CircleAuthorizationToFile); + } + + [Fact] + public void Deleting_circle_cascades_file_acl_rows() + { + using var scope = CreateScope(); + + scope.Service.SetAccess(scope.Circle.Id, "alice/documents/report.txt", FileAccessRight.Read); + scope.Db.Circle.Remove(scope.Circle); + scope.Db.SaveChanges(); + + Assert.Empty(scope.Db.CircleAuthorizationToFile); + } + + private static TestScope CreateScope() + { + var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + + var db = new ApplicationDbContext(options); + db.Database.EnsureCreated(); + + db.Users.Add(new ApplicationUser + { + Id = "alice", + UserName = "alice", + Email = "alice@example.test" + }); + db.SaveChanges(); + + var circle = new Circle + { + OwnerId = "alice", + Name = "shared", + Public = false + }; + + db.Circle.Add(circle); + db.SaveChanges(); + + var service = new FileSystemAuthManager(db, Options.Create(new SiteSettings())); + return new TestScope(connection, db, service, circle); + } + + private sealed class TestScope : IDisposable + { + public TestScope(SqliteConnection connection, ApplicationDbContext db, FileSystemAuthManager service, Circle circle) + { + Connection = connection; + Db = db; + Service = service; + Circle = circle; + } + + public SqliteConnection Connection { get; } + public ApplicationDbContext Db { get; } + public FileSystemAuthManager Service { get; } + public Circle Circle { get; } + + public void Dispose() + { + Db.Dispose(); + Connection.Dispose(); + } + } +} diff --git a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj index 106079202..0d18d1e0f 100644 --- a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj +++ b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj @@ -53,6 +53,7 @@ +