code reorg

This commit is contained in:
Paul Schneider 2026-09-07 14:39:53 +01:00
commit d8c0678462
Signed by: notazof
GPG key ID: 1DD5D838E5343B06
11 changed files with 154 additions and 22 deletions

View file

@ -148,7 +148,7 @@ private void ConfigureRootView(MainView rootView)
public static async Task PushBlogsPageAsync() public static async Task PushBlogsPageAsync()
{ {
var app = (App)Current!; var app = (App)Current!;
var mainVm = app.ServiceProvider!.GetRequiredService<MainViewModel>(); var mainVm = app.ServiceProvider!.GetRequiredService<BlogsViewModel>();
await mainVm.InitializeAsync(); await mainVm.InitializeAsync();
await app.PushPageAsync(mainVm); await app.PushPageAsync(mainVm);
} }

View file

@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
using PostIt.Services; using PostIt.Services;
using PostIt.ViewModels; using PostIt.ViewModels;
using PostIt.Views; using PostIt.Views;
using PostIt.Views.Blogs;
using PostIt.Views.Commands; using PostIt.Views.Commands;
using Yavsc.Api.Client; using Yavsc.Api.Client;
@ -34,7 +35,7 @@ public static class ServiceCollectionHelpers
// Vues // Vues
services.AddSingleton<MainView>(); services.AddSingleton<MainView>();
services.AddSingleton<MainPage>(); services.AddSingleton<BlogsPage>();
services.AddSingleton<MainWindow>(); services.AddSingleton<MainWindow>();
// SettingsPage is a singleton: there must be one and only one // SettingsPage is a singleton: there must be one and only one
@ -89,7 +90,7 @@ public static class ServiceCollectionHelpers
sessionStatus.Refresh(); sessionStatus.Refresh();
services.AddSingleton(sessionStatus); services.AddSingleton(sessionStatus);
services.AddSingleton<SessionStatusBanner>(); services.AddSingleton<SessionStatusBanner>();
services.AddSingleton<MainViewModel>(); services.AddSingleton<BlogsViewModel>();
return services.BuildServiceProvider(); return services.BuildServiceProvider();
} }
} }

View file

@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
using PostIt.ViewModels; using PostIt.ViewModels;
using PostIt.ViewModels.Commands; using PostIt.ViewModels.Commands;
using PostIt.Views; using PostIt.Views;
using PostIt.Views.Blogs;
using PostIt.Views.Commands; using PostIt.Views.Commands;
namespace PostIt; namespace PostIt;
@ -38,7 +39,7 @@ public class ViewLocator : IDataTemplate
var services = app!.ServiceProvider!; var services = app!.ServiceProvider!;
return data switch return data switch
{ {
MainViewModel => services.GetRequiredService<MainPage>(), BlogsViewModel => services.GetRequiredService<BlogsPage>(),
Settings => services.GetRequiredService<SettingsPage>(), Settings => services.GetRequiredService<SettingsPage>(),
HomePageViewModel => services.GetRequiredService<HomePage>(), HomePageViewModel => services.GetRequiredService<HomePage>(),
ActivitiesPageViewModel => services.GetRequiredService<ActivitiesPage>(), ActivitiesPageViewModel => services.GetRequiredService<ActivitiesPage>(),

View file

@ -12,7 +12,7 @@ using PostIt.Helpers;
namespace PostIt.ViewModels; namespace PostIt.ViewModels;
public partial class MainViewModel : ViewModelBase, IActionStatusViewModel public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
{ {
/// <summary>Window/tab title. Cosmetic — bound by /// <summary>Window/tab title. Cosmetic — bound by
/// <c>MainPage.axaml</c> if at all. Not the post title.</summary> /// <c>MainPage.axaml</c> if at all. Not the post title.</summary>
@ -336,7 +336,7 @@ public partial class MainViewModel : ViewModelBase, IActionStatusViewModel
} }
public MainViewModel() public BlogsViewModel()
{ {
SettingsModel = new Settings(); SettingsModel = new Settings();
Init(SettingsModel); Init(SettingsModel);
@ -396,7 +396,7 @@ public partial class MainViewModel : ViewModelBase, IActionStatusViewModel
/// <see cref="BlogApiClient"/>. Production code uses the /// <see cref="BlogApiClient"/>. Production code uses the
/// (Settings, BlogApiClient) overload below. /// (Settings, BlogApiClient) overload below.
/// </summary> /// </summary>
public MainViewModel(BlogApiClient blogClient, Settings? settings = null, IServiceProvider? services = null) public BlogsViewModel(BlogApiClient blogClient, Settings? settings = null, IServiceProvider? services = null)
{ {
SettingsModel = new Settings(); SettingsModel = new Settings();
BlogClient = blogClient ?? throw new ArgumentNullException(nameof(blogClient)); ; BlogClient = blogClient ?? throw new ArgumentNullException(nameof(blogClient)); ;

View file

@ -5,15 +5,15 @@
xmlns:vm="using:PostIt.ViewModels" xmlns:vm="using:PostIt.ViewModels"
xmlns:postitControls="using:PostIt.Controls" xmlns:postitControls="using:PostIt.Controls"
xmlns:models="using:Yavsc.Blogspot" xmlns:models="using:Yavsc.Blogspot"
xmlns:views="using:PostIt.Views" xmlns:views="using:PostIt.Views.Blogs"
xmlns:AvaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit" xmlns:AvaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
mc:Ignorable="d" mc:Ignorable="d"
x:Class="PostIt.Views.MainPage" x:Class="PostIt.Views.Blogs.BlogsPage"
x:DataType="vm:MainViewModel" x:DataType="vm:BlogsViewModel"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<Design.DataContext> <Design.DataContext>
<vm:MainViewModel /> <vm:BlogsViewModel />
</Design.DataContext> </Design.DataContext>
<Grid Margin="12" RowSpacing="12" <Grid Margin="12" RowSpacing="12"

View file

@ -2,11 +2,11 @@ using System;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
namespace PostIt.Views; namespace PostIt.Views.Blogs;
public partial class MainPage : ContentPage public partial class BlogsPage : ContentPage
{ {
public MainPage() public BlogsPage()
{ {
InitializeComponent(); InitializeComponent();
} }
@ -14,7 +14,7 @@ public partial class MainPage : ContentPage
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
if (DataContext is ViewModels.MainViewModel vm) if (DataContext is ViewModels.BlogsViewModel vm)
{ {
if (!vm.IsLoaded) if (!vm.IsLoaded)
{ {

View file

@ -6,11 +6,11 @@
xmlns:views="using:PostIt.Views" xmlns:views="using:PostIt.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="PostIt.Views.MainView" x:Class="PostIt.Views.MainView"
x:DataType="vm:MainViewModel"> x:DataType="vm:HomePageViewModel">
<Design.DataContext> <Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE, <!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) --> to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainViewModel /> <vm:HomePageViewModel />
</Design.DataContext> </Design.DataContext>
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">

View file

@ -13,7 +13,7 @@ public partial class MainView : UserControl
protected override void OnDataContextChanged(EventArgs e) protected override void OnDataContextChanged(EventArgs e)
{ {
base.OnDataContextChanged(e); base.OnDataContextChanged(e);
if (DataContext is ViewModels.MainViewModel vm) if (DataContext is ViewModels.BlogsViewModel vm)
{ {
if (!vm.IsLoaded) if (!vm.IsLoaded)
{ {

View file

@ -4,8 +4,8 @@
<!-- Yavsc.Org.Tests-specific versions --> <!-- Yavsc.Org.Tests-specific versions -->
<ItemGroup> <ItemGroup>
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.9" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.9" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.9" /> <PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.11" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -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<ApplicationDbContext>()
.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();
}
}
}

View file

@ -53,6 +53,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
</ItemGroup> </ItemGroup>
<!-- <!--
MapStaticAssets() in the production pipeline resolves MapStaticAssets() in the production pipeline resolves