Merge pull request 'release/1.0.8-rc4' (#44) from release/1.0.8-rc4 into main
All checks were successful
Dotnet build and test / build (push) Successful in 5m49s
Forgejo Release / release (push) Successful in 10m17s

Reviewed-on: #44
This commit is contained in:
Paul Schneider 2026-08-28 22:52:44 +01:00
commit 4747942f93
39 changed files with 343 additions and 217 deletions

View file

@ -42,5 +42,6 @@
"copilotcli/gpt-5.3-codex"
]
}
}
},
"dotnet.defaultSolution": "yavsc.sln"
}

32
.vscode/tasks.json vendored
View file

@ -1,5 +1,21 @@
{
"version": "2.0.0",
"isRoot": true,
"problemMatcher": [
{
"owner": "dotnet",
"fileLocation": ["relative", "${workspaceFolder}"],
"source": "dotnet",
"pattern": {
"regexp": "^\\s+(.*)\\((\\d+):(\\d+)\\):\\s+(error|warning)\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
}
],
"tasks": [
{
"label": "run-debug-android",
@ -20,21 +36,6 @@
"-p:AndroidAttachDebugger=true",
"-p:AndroidSdbHostPort=55555",
"-p:AndroidSdbTargetPort=55555"
],
"problemMatcher": [
{
"owner": "dotnet",
"fileLocation": ["relative", "${workspaceFolder}"],
"source": "dotnet",
"pattern": {
"regexp": "^(.*):(\\d+):(\\d+):\\s+(error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
}
]
},
{
@ -45,7 +46,6 @@
"group": "build",
"isBuildCommand": true,
"isTestCommand": false,
"problemMatcher": ["$msCompile"],
"isBackground": true
},
{

View file

@ -16,6 +16,16 @@ Cette convention est partagée avec le dépôt
[`postit-debian`](https://forgejo.pschneider.fr/notazof/postit-debian)
pour la production des paquets `.deb`.
## [1.0.8-rc4] - unstable
### Added
### Changed
### Fixed
* [TODO] bug loading a blog post from PostIt, ACL come along with and don't need any "Refresh" button.
## [1.0.8-rc3] - unstable
### Added

View file

@ -9,23 +9,22 @@
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>
<AndroidResource Include="Icon.png">
<Link>Resources\drawable\Icon.png</Link>
</AndroidResource>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Android" />
<PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" />
<PackageReference Include="Xamarin.AndroidX.Browser" />
</ItemGroup>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PostIt\PostIt.csproj" />
</ItemGroup>
</Project>
</Project>

View file

@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>
@ -15,4 +15,4 @@
<ItemGroup>
<ProjectReference Include="..\PostIt\PostIt.csproj" />
</ItemGroup>
</Project>
</Project>

View file

@ -7,7 +7,7 @@
<Nullable>enable</Nullable>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<PropertyGroup>
@ -24,4 +24,4 @@
<ItemGroup>
<ProjectReference Include="..\PostIt\PostIt.csproj" />
</ItemGroup>
</Project>
</Project>

View file

@ -8,7 +8,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>
@ -16,9 +16,7 @@
<PackageReference Include="Xamarin.UITest" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Avalonia.Headless" />
<PackageReference Include="Avalonia.Headless.XUnit" />
</ItemGroup>
@ -28,6 +26,5 @@
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
</ItemGroup>
</Project>
<ItemGroup></ItemGroup>
</Project>

View file

@ -1,14 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<Content Include="postit-settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -31,9 +33,8 @@
<PackageReference Include="IdentityModel.OidcClient" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../Yavsc.Abstract/Yavsc.Abstract.csproj" />
<ProjectReference Include="../../Yavsc.Api.Client/Yavsc.Api.Client.csproj" />
</ItemGroup>
</Project>
</Project>

View file

@ -1,72 +0,0 @@
using System;
using System.Threading.Tasks;
using Avalonia.Threading;
namespace PostIt.Services;
/// <summary>
/// Tiny marshalling helper around <see cref="Dispatcher.UIThread"/> so
/// the rest of the codebase does not have to import Avalonia.Threading
/// directly. We want exactly one place that decides "is the current
/// thread the Avalonia UI thread, and if not, post there" so that
/// <see cref="ObservableObject"/>-derived types (Settings, the various
/// ViewModels) can fire <c>PropertyChanged</c> safely from background
/// work — which is exactly the cross-thread case that previously blew
/// up inside <c>DataValidationErrors.SetErrors</c> on Avalonia 11.
///
/// The helper is intentionally tiny: a sync post when we are off the
/// UI thread, a no-op when we are already on it, and an async fire-
/// and-forget variant for places where awaiting would deadlock the
/// caller (e.g. <c>Settings.Load</c> continuation paths).
/// </summary>
public static class UiDispatcher
{
/// <summary>
/// True when the calling thread is the Avalonia UI thread. Property
/// setters that touch bindings should check this before mutating
/// state; the safe path is <see cref="InvokeIfNeeded"/>.
/// </summary>
public static bool IsOnUiThread => Dispatcher.UIThread.CheckAccess();
/// <summary>
/// Run <paramref name="action"/> on the UI thread. If the caller is
/// already on the UI thread, run synchronously to preserve stack
/// traces and ordering; otherwise post to the dispatcher and wait.
/// Never throws on shutdown — a missing dispatcher is treated as
/// "best-effort skipped", matching Avalonia's own behaviour when
/// the application lifetime has been torn down.
/// </summary>
public static void InvokeIfNeeded(Action action)
{
if (action is null) return;
if (IsOnUiThread) { action(); return; }
try { Dispatcher.UIThread.Post(action, DispatcherPriority.Normal); }
catch (InvalidOperationException) { /* dispatcher gone, nothing to do */ }
}
/// <summary>
/// Fire-and-forget variant: schedules <paramref name="action"/> on
/// the UI thread but does not block the caller. Use this from
/// background workers (OIDC discovery, HTTP callbacks, file I/O)
/// where awaiting the dispatcher would deadlock the calling sync
/// context.
/// </summary>
public static void Post(Action action)
{
if (action is null) return;
try { Dispatcher.UIThread.Post(action, DispatcherPriority.Normal); }
catch (InvalidOperationException) { /* dispatcher gone */ }
}
/// <summary>
/// Awaitable variant. Useful inside <c>async</c> ViewModel methods
/// that must touch bindings only after the dispatcher has processed
/// a queued update (e.g. "load file then refresh observable state").
/// </summary>
public static Task InvokeAsync(Action action)
{
if (action is null) return Task.CompletedTask;
if (IsOnUiThread) { action(); return Task.CompletedTask; }
return Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Normal).GetTask();
}
}

View file

@ -7,6 +7,13 @@ namespace Yavsc
{
public const string APIPrefix = "api/v1";
public const string BlogSpotPath = "blogspot";
public const string BlogAclPath = "blogacl";
public const string BlogTagPath = "blogtag";
public const string CirclePath = "circle";
public const string CommentsPath = "blogcomments";
public static readonly Scope[] SiteScopes = {
new Scope { Id = "profile", Description = "Your profile informations" },
new Scope { Id = "book" , Description ="Your booking interface"},

View file

@ -11,10 +11,10 @@
<LangVersion>latest</LangVersion>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" />
</ItemGroup>
</Project>
</Project>

View file

@ -17,7 +17,7 @@
<Library>true</Library>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>
@ -26,4 +26,4 @@
<ItemGroup>
<ProjectReference Include="../Yavsc.Abstract/Yavsc.Abstract.csproj" />
</ItemGroup>
</Project>
</Project>

View file

@ -7,7 +7,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>
@ -17,4 +17,4 @@
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" />
</ItemGroup>
</Project>
</Project>

View file

@ -1,10 +1,12 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Yavsc.Abstract.BlogSpot;
using Yavsc.Models;
using Yavsc.Models.Access;
using Yavsc.Models.Blog;
using Yavsc.Tests.Shared;
using static Yavsc.Constants;
@ -38,15 +40,16 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
{
private readonly BlogsWebServerFixture _fixture;
public BlogAclApiTests(BlogsWebServerFixture fixture)
{
_fixture = fixture;
}
private string BlogUrl()
=> $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/{BlogSpotPath}";
private string BlogAclUrl()
=> $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/blogacl";
=> $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/{BlogAclPath}";
/// <summary>Delete any ACL rows tied to the fixture's seeded
/// <c>(CircleId, BlogPostId)</c> pair. The shared SQLite store
@ -120,7 +123,7 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
// owned by the caller. We seed the same shape pre-POST so the
// test reproduces the prod scenario end-to-end.
CleanupAcl();
using var http = NewClient("alice");
using var http = NewClient(_fixture.DefaultUserLogin);
var payload = new PostAccessControlRulePayload
{
@ -128,7 +131,8 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
BlogPostId = _fixture.PostId
};
var response = await http.PostAsJsonAsync(BlogAclUrl(), payload,
var response = await http.PostAsJsonAsync(
BlogAclUrl(), payload,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
@ -178,7 +182,7 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
[MemberData(nameof(BlogAclPayloadsForNever500))]
public async Task PostCircleAuthorization_never_returns_500(PostAccessControlRulePayload payload)
{
using var http = NewClient("alice");
using var http = NewClient(_fixture.DefaultUserLogin);
var response = await http.PostAsJsonAsync(
BlogAclUrl(), payload,
@ -216,4 +220,83 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
);
}
[Fact]
public async Task PostBlog_with_ACL_creates_a_post_and_Get_returns_it_in_the_list()
{
CleanupAcl();
_fixture.SeedUser(_fixture.DefaultUserLogin);
_fixture.SeedUser("tester");
_fixture.SeedCircle(_fixture.DefaultUserLogin, "test",
false,
new String[]
{
_fixture.DefaultUserLogin,
"tester"
});
using var http = NewClient(_fixture.DefaultUserLogin );
// Create a minimal BlogPost. The server assigns Id, so we
// send 0 + an explicit AuthorId; the production
// BlogSpotService.Create() tolerates that.
var draft = new BlogPost
{
Id = 0,
Title = "Premier billet",
AuthorId = "tester",
Article = "Contenu de test.",
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
ACL = new List<CircleAuthorizationToBlogPost>(
new CircleAuthorizationToBlogPost[]
{
new CircleAuthorizationToBlogPost
{
CircleId = _fixture.CircleId,
BlogPostId = _fixture.PostId
}
}
)
};
var postResponse = await http.PostAsJsonAsync(
BlogUrl(),
draft,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode);
// The POST returns the server-issued post (with a real Id).
var created = await postResponse.Content.ReadFromJsonAsync<BlogPost>(
TestContext.Current.CancellationToken
);
Assert.NotNull(created);
Assert.NotEqual(0, created!.Id);
Assert.Equal(draft.Title, created.Title);
// The list should now contain exactly one entry.
var listResponse = await http.GetAsync(
BlogUrl(),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
using var doc = JsonDocument.Parse(await listResponse.Content.ReadAsStringAsync(
TestContext.Current.CancellationToken
));
Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
Assert.Equal(2, doc.RootElement.GetArrayLength());
Assert.Equal(created.Id, doc.RootElement[0].GetProperty("id").GetInt64());
// detail should return the same post, with ACL and tags.
var detailResponse = await http.GetAsync(
$"{BlogUrl()}/{created.Id}",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, detailResponse.StatusCode);
using var detailDoc = JsonDocument.Parse(await detailResponse.Content.ReadAsStringAsync(
TestContext.Current.CancellationToken
));
Assert.Equal(JsonValueKind.Object, detailDoc.RootElement.ValueKind);
Assert.Equal(created.Id, detailDoc.RootElement.GetProperty("id").GetInt64());
Assert.Equal(1, detailDoc.RootElement.GetProperty("acl").GetArrayLength());
}
}

View file

@ -8,14 +8,21 @@ using Microsoft.IdentityModel.Tokens;
using Yavsc.Models;
using Yavsc.Models.Blog;
using Yavsc.Tests.Shared;
using Yavsc.Blogs.Tests.Fixtures;
namespace Yavsc.Blogs.Tests;
[Collection("JwtClaimMapping")]
public sealed class BlogApiMappedClaimsTests : IClassFixture<MappedClaimsBlogsWebServerFixture>
public sealed class BlogApiMappedClaimsTests :
IClassFixture<MappedClaimsBlogsWebServerFixture>,
IBackendFixture
{
private readonly MappedClaimsBlogsWebServerFixture _fixture;
public IReadOnlyList<string> Addresses => throw new NotImplementedException();
public IServiceProvider Services => throw new NotImplementedException();
public BlogApiMappedClaimsTests(MappedClaimsBlogsWebServerFixture fixture)
{
_fixture = fixture;
@ -79,7 +86,10 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture<MappedClaimsBlogsWe
DateModified = DateTime.UtcNow
};
var response = await http.PostAsJsonAsync("/api/v1/blog", draft, TestContext.Current.CancellationToken);
var response = await http.PostAsJsonAsync(
_fixture.BlogSpotUrl(),
draft,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var created = await response.Content.ReadFromJsonAsync<BlogPost>(TestContext.Current.CancellationToken);
@ -93,7 +103,7 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture<MappedClaimsBlogsWe
ResetDatabase();
using var http = NewClient(subject: "mapped-owner");
var createdResponse = await http.PostAsJsonAsync("/api/v1/blog", new BlogPost
var createdResponse = await http.PostAsJsonAsync(_fixture.BlogSpotUrl(), new BlogPost
{
Id = 0,
Title = "Billet à modifier",
@ -107,7 +117,7 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture<MappedClaimsBlogsWe
var created = await createdResponse.Content.ReadFromJsonAsync<BlogPost>(TestContext.Current.CancellationToken);
Assert.NotNull(created);
var updateResponse = await http.PutAsJsonAsync($"/api/v1/blog/{created!.Id}", new BlogPost
var updateResponse = await http.PutAsJsonAsync(_fixture.BlogSpotUrl() + $"/{created!.Id}", new BlogPost
{
Id = created.Id,
Title = "Billet modifié",
@ -126,7 +136,7 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture<MappedClaimsBlogsWe
ResetDatabase();
using var ownerHttp = NewClient(subject: "mapped-owner");
var createdResponse = await ownerHttp.PostAsJsonAsync("/api/v1/blog", new BlogPost
var createdResponse = await ownerHttp.PostAsJsonAsync(_fixture.BlogSpotUrl(), new BlogPost
{
Id = 0,
Title = "Billet protégé",
@ -141,7 +151,7 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture<MappedClaimsBlogsWe
Assert.NotNull(created);
using var otherHttp = NewClient(subject: "mapped-other");
var updateResponse = await otherHttp.PutAsJsonAsync($"/api/v1/blog/{created!.Id}", new BlogPost
var updateResponse = await otherHttp.PutAsJsonAsync(_fixture.BlogSpotUrl() + $"/{created!.Id}", new BlogPost
{
Id = created.Id,
Title = "Tentative de modification",

View file

@ -2,11 +2,11 @@ using System.Net;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Yavsc.Models;
using Yavsc.Models.Blog;
using Yavsc.Server.Helpers;
using Yavsc.Tests.Shared;
using Yavsc.Blogs.Tests.Fixtures;
namespace Yavsc.Blogs.Tests;
@ -31,18 +31,6 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
_fixture = fixture;
}
/// <summary>Reset the in-memory database to a known empty state.
/// <c>UseInMemoryDatabase</c> shares its store across the
/// lifetime of the <see cref="BlogsWebServerFixture"/> instance,
/// so without a per-test reset the test order would leak
/// state between tests.</summary>
private void ResetDatabase()
{
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
}
/// <summary>Reset the database and seed the
/// <c>tester</c> <see cref="ApplicationUser"/> row. Required
@ -55,18 +43,10 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
/// at <c>SaveChanges</c> and the controller returns 500.</summary>
private void ResetAndSeedDefaultUser()
{
ResetDatabase();
_fixture.ResetDatabase();
_fixture.SeedUser("tester");
}
/// <summary>The fixture's <c>WebApplication</c> is bound to
/// <c>https://localhost:&lt;random&gt;</c> via
/// <see cref="WebHostFixture.Addresses"/>. We pick the first
/// https URL and append the controller route
/// (<c>/api/v1/blog</c>, matching the production
/// <c>[Route(APIPrefix + "/blog")]</c>).</summary>
private string BlogsUrl =>
_fixture.Addresses.First(a => a.StartsWith("https://")) + "/api/v1/blog";
/// <summary>Build an authenticated client: a real
/// <c>Authorization: Bearer &lt;jwt&gt;</c> header where the JWT
@ -111,10 +91,11 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
[Fact]
public async Task GetBlogs_returns_200_with_empty_list_when_no_posts()
{
ResetDatabase();
_fixture.ResetDatabase();
using var http = NewClient();
var response = await http.GetAsync("/api/v1/blog",
var response = await http.GetAsync(
_fixture.BlogSpotUrl(),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -147,7 +128,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateModified = DateTime.UtcNow
};
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft,
var postResponse = await http.PostAsJsonAsync(_fixture.BlogSpotUrl(), draft,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode);
@ -160,7 +141,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
Assert.Equal(draft.Title, created.Title);
// The list should now contain exactly one entry.
var listResponse = await http.GetAsync("/api/v1/blog",
var listResponse = await http.GetAsync(_fixture.BlogSpotUrl(),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
@ -188,7 +169,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateModified = DateTime.UtcNow
};
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft,
var postResponse = await http.PostAsJsonAsync(_fixture.BlogSpotUrl(), draft,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode);
@ -198,7 +179,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
Assert.NotNull(created);
Assert.Equal("tester", created!.AuthorId);
var listResponse = await http.GetAsync("/api/v1/blog",
var listResponse = await http.GetAsync(_fixture.BlogSpotUrl(),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
@ -226,7 +207,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateModified = DateTime.UtcNow
};
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft,
var postResponse = await http.PostAsJsonAsync(_fixture.BlogSpotUrl(), draft,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode);
@ -266,7 +247,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
[Fact]
public async Task GetBlog_returns_401_when_no_token_is_provided()
{
ResetDatabase();
_fixture.ResetDatabase();
using var http = NewAnonymousClient();
// No Authorization header → the JwtBearer middleware
@ -275,7 +256,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
// the framework returns 401. This is the proof that the
// production policy is wired in the test host and not
// short-circuited by a test-only auth bypass.
var response = await http.GetAsync("/api/v1/blog",
var response = await http.GetAsync(_fixture.BlogSpotUrl(),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
@ -303,7 +284,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow
};
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft,
var postResponse = await http.PostAsJsonAsync(_fixture.BlogSpotUrl(), draft,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode);
@ -322,13 +303,13 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateCreated = created.DateCreated,
DateModified = DateTime.UtcNow
};
var putResponse = await http.PutAsJsonAsync($"/api/v1/blog/{created.Id}",
var putResponse = await http.PutAsJsonAsync(_fixture.BlogSpotUrl()+$"/{created.Id}",
update,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NoContent, putResponse.StatusCode);
// The list should now reflect the new title.
var listResponse = await http.GetAsync("/api/v1/blog",
var listResponse = await http.GetAsync(_fixture.BlogSpotUrl(),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
using var doc = JsonDocument.Parse(
@ -356,19 +337,19 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow
};
var postResponse = await http.PostAsJsonAsync("/api/v1/blog", draft,
var postResponse = await http.PostAsJsonAsync(_fixture.BlogSpotUrl(), draft,
TestContext.Current.CancellationToken);
var created = (await postResponse.Content.ReadFromJsonAsync<BlogPost>(
TestContext.Current.CancellationToken
))!;
var deleteResponse = await http.DeleteAsync($"/api/v1/blog/{created.Id}",
var deleteResponse = await http.DeleteAsync(_fixture.BlogSpotUrl()+$"/{created.Id}",
TestContext.Current.CancellationToken
);
Assert.Equal(HttpStatusCode.OK, deleteResponse.StatusCode);
// The list should now be empty.
var listResponse = await http.GetAsync("/api/v1/blog",
var listResponse = await http.GetAsync(_fixture.BlogSpotUrl(),
TestContext.Current.CancellationToken);
String response = await listResponse.Content.ReadAsStringAsync(
TestContext.Current.CancellationToken
@ -411,7 +392,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateModified = DateTime.UtcNow
};
var response = await http.PostAsJsonAsync("/api/v1/blog", draft,
var response = await http.PostAsJsonAsync(_fixture.BlogSpotUrl(), draft,
TestContext.Current.CancellationToken);
// Dump the body on failure so the test name + the response
@ -443,7 +424,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
// behaviour so a future change that, say, makes Title
// nullable in the model or drops [Required], triggers a
// conscious update of the test (and probably of the VM).
ResetDatabase();
_fixture.ResetDatabase();
using var http = NewClient(subject: "tester");
var draft = new BlogPost
@ -456,7 +437,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateModified = DateTime.UtcNow
};
var response = await http.PostAsJsonAsync("/api/v1/blog", draft,
var response = await http.PostAsJsonAsync(_fixture.BlogSpotUrl(), draft,
TestContext.Current.CancellationToken);
if (response.StatusCode != HttpStatusCode.BadRequest)

View file

@ -10,7 +10,7 @@ using Yavsc.Models.Blog;
using Yavsc.Models.Relationship;
using Yavsc.Services;
using Yavsc.Tests.Shared;
using static Yavsc.Constants;
namespace Yavsc.Blogs.Tests;
/// <summary>
@ -61,6 +61,7 @@ public sealed class BlogsWebServerFixture : WebHostFixture
public long CircleId { get; private set; }
public long PostId { get; private set; }
public string DefaultUserLogin { get => "alice"; }
// A single SqliteConnection held open at the static level,
// mirroring how Yavsc.Org.Tests.WebServerFixture hoists its
@ -169,7 +170,8 @@ public sealed class BlogsWebServerFixture : WebHostFixture
// PermissionHandler ownership check sees a null
// user id and rejects every PUT.
options.MapInboundClaims = false;
options.TokenValidationParameters = new TokenValidationParameters
options.TokenValidationParameters
= new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = TestTokenIssuer.Issuer,
@ -263,6 +265,27 @@ public sealed class BlogsWebServerFixture : WebHostFixture
await Task.CompletedTask;
return app;
}
/// <summary>Reset the in-memory database to a known empty state.
/// <c>UseInMemoryDatabase</c> shares its store across the
/// lifetime of the <see cref="BlogsWebServerFixture"/> instance,
/// so without a per-test reset the test order would leak
/// state between tests.</summary>
public void ResetDatabase()
{
using var scope = Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
}
public void CleanupAcl()
{
using var scope = Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
db.CircleAuthorizationToBlogPost
.Where(a => a.CircleId == CircleId
&& a.BlogPostId == PostId)
.ExecuteDelete();
}
public override void Dispose()
{
@ -310,7 +333,8 @@ public sealed class BlogsWebServerFixture : WebHostFixture
/// <param name="configure">Optional hook to fill in fields
/// like <c>FullName</c> / <c>Avatar</c> / <c>EmailConfirmed</c>
/// that downstream tests assert on.</param>
public ApplicationUser SeedUser(string userName, Action<ApplicationUser>? configure = null)
public ApplicationUser SeedUser(string userName,
Action<ApplicationUser>? configure = null)
{
using var scope = Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
@ -351,20 +375,31 @@ public sealed class BlogsWebServerFixture : WebHostFixture
/// <summary>Create a circle owned by <paramref name="ownerId"/>
/// directly in the SQLite store and return its server-assigned
/// id.</summary>
private long SeedCircle(string ownerId, string name, bool isPublic = false)
public long SeedCircle(string ownerId, string name, bool isPublic = false,
ICollection<String> members = null
)
{
using var scope = Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var circle = new Circle { OwnerId = ownerId, Name = name, Public = isPublic };
db.Circle.Add(circle);
db.SaveChanges();
if (members != null && members.Count > 0)
{
foreach (String memberId in members)
{
var member = new CircleMember { CircleId = circle.Id, MemberId = memberId };
db.CircleMembers.Add(member);
}
db.SaveChanges();
}
return circle.Id;
}
/// <summary>Create a blog post owned by <paramref name="authorId"/>
/// directly in the SQLite store and return its server-assigned
/// id.</summary>
private long SeedBlogPost(string authorId, string title)
public long SeedBlogPost(string authorId, string title)
{
using var scope = Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
@ -380,4 +415,5 @@ public sealed class BlogsWebServerFixture : WebHostFixture
db.SaveChanges();
return post.Id;
}
}

View file

@ -21,7 +21,7 @@ namespace Yavsc.Blogs.Tests;
/// This is the closest in-process reproduction of the production
/// authentication surface for the blog API.
/// </summary>
public sealed class MappedClaimsBlogsWebServerFixture : IDisposable
public sealed class MappedClaimsBlogsWebServerFixture : IDisposable, IBackendFixture
{
private readonly InMemoryDatabaseRoot _inMemoryRoot = new();
private readonly Dictionary<string, string> _savedInboundMap;

View file

@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Yavsc.Models;
using Yavsc.Models.Blog;
using Yavsc.Tests.Shared;
using Yavsc.Blogs.Tests.Fixtures;
namespace Yavsc.Blogs.Tests;
@ -71,12 +72,6 @@ public sealed class PublishEndpointTests : IClassFixture<BlogsWebServerFixture>
return post.Id;
}
private string PublishUrl(long id)
=> $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/api/v1/blog/{id}/publish";
private string BlogsUrl
=> _fixture.Addresses.First(a => a.StartsWith("https://")) + "/api/v1/blog";
private HttpClient NewClient(string subject)
{
var handler = new HttpClientHandler
@ -100,12 +95,13 @@ public sealed class PublishEndpointTests : IClassFixture<BlogsWebServerFixture>
var postId = SeedPost("alice");
using var http = NewClient("alice");
var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }, TestContext.Current.CancellationToken);
var put = await http.PutAsJsonAsync(_fixture.PublishUrl(postId), new { publish = true }, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NoContent, put.StatusCode);
var get = await http.GetAsync($"{BlogsUrl}/{postId}", TestContext.Current.CancellationToken);
var get = await http.GetAsync(_fixture.BlogSpotUrl() + $"/{postId}", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, get.StatusCode);
using var doc = JsonDocument.Parse(await get.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
Assert.Equal($"post-by-alice", doc.RootElement.GetProperty("title").GetString());
Assert.True(doc.RootElement.GetProperty("isPublished").GetBoolean());
}
@ -116,11 +112,12 @@ public sealed class PublishEndpointTests : IClassFixture<BlogsWebServerFixture>
var postId = SeedPost("alice");
using var http = NewClient("alice");
await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }, TestContext.Current.CancellationToken);
var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = false }, TestContext.Current.CancellationToken);
await http.PutAsJsonAsync(_fixture.PublishUrl(postId), new { publish = true }, TestContext.Current.CancellationToken);
var put = await http.PutAsJsonAsync(_fixture.PublishUrl(postId), new { publish = false }, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NoContent, put.StatusCode);
var get = await http.GetAsync($"{BlogsUrl}/{postId}", TestContext.Current.CancellationToken);
var get = await http.GetAsync(_fixture.BlogSpotUrl() + $"/{postId}", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, get.StatusCode);
using var doc = JsonDocument.Parse(await get.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
Assert.False(doc.RootElement.GetProperty("isPublished").GetBoolean());
}
@ -130,7 +127,7 @@ public sealed class PublishEndpointTests : IClassFixture<BlogsWebServerFixture>
{
ResetDatabase();
using var http = NewClient("alice");
var put = await http.PutAsJsonAsync(PublishUrl(99999L), new { publish = true }, TestContext.Current.CancellationToken);
var put = await http.PutAsJsonAsync(_fixture.PublishUrl(99999L), new { publish = true }, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, put.StatusCode);
}
@ -141,7 +138,7 @@ public sealed class PublishEndpointTests : IClassFixture<BlogsWebServerFixture>
var postId = SeedPost("alice");
using var http = NewClient("bob");
var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }, TestContext.Current.CancellationToken);
var put = await http.PutAsJsonAsync(_fixture.PublishUrl(postId), new { publish = true }, TestContext.Current.CancellationToken);
// 401 Challenge (the controller returns Challenge()
// for AuthorizationFailureException). The exact code
// is framework-dependent; what matters is "not 204".

View file

@ -9,7 +9,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>
@ -35,4 +35,4 @@
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" />
</ItemGroup>
</Project>
</Project>

View file

@ -1,6 +1,6 @@
namespace Yavsc.Blogs;
public static class Constants
public static class BlogConstants
{
public const string AdminRole = "Admin";
public const string ModeratorRole = "Moderator";

View file

@ -9,7 +9,7 @@ namespace Yavsc.Blogs.Controllers
{
[Authorize("BlogScope")]
[Produces("application/json")]
[Route(APIPrefix + "/blog")]
[Route(APIPrefix + "/" + BlogSpotPath)]
public class BlogApiController : Controller
{
private readonly BlogSpotService blogSpotService;
@ -19,14 +19,14 @@ namespace Yavsc.Blogs.Controllers
this.blogSpotService = blogSpotService;
}
// GET: api/BlogApi
// GET: api/v1/blogspot
[HttpGet]
public async Task<IEnumerable<IBlogPost>> GetBlogspot(int start = 0, int take = 25)
{
return await blogSpotService.Index(User, null, start, take);
}
// GET: api/BlogApi/5
// GET: api/v1/blogspot/5
[HttpGet("{id}", Name = "GetBlog")]
public async Task<IActionResult> GetBlog([FromRoute] long id)
{
@ -43,7 +43,7 @@ namespace Yavsc.Blogs.Controllers
return NotFound();
}
return Ok(blog);
return Ok(blog.GetPayload());
}
catch (AuthorizationFailureException)
{
@ -51,7 +51,7 @@ namespace Yavsc.Blogs.Controllers
}
}
// PUT: api/BlogApi/5
// PUT: api/v1/blogspot/5
[HttpPut("{id}")]
public async Task<IActionResult> PutBlog(long id, [FromBody] Models.Blog.BlogPost blog)
{
@ -83,7 +83,7 @@ namespace Yavsc.Blogs.Controllers
return new StatusCodeResult(StatusCodes.Status204NoContent);
}
// POST: api/v1/blog
// POST: api/v1/blogspot
[HttpPost]
public IActionResult PostBlog([FromBody] Models.Blog.BlogPost blog)
{
@ -116,7 +116,8 @@ namespace Yavsc.Blogs.Controllers
: (IFormFileCollection)new FormFileCollection();
var uid = User.GetUserId();
var post = blogSpotService.Create(uid, blog, files);
return CreatedAtRoute("GetBlog", new { id = post.Id }, post);
return CreatedAtRoute("GetBlog", new { id = post.Id },
post.GetPayload());
}
// DELETE: api/BlogApi/5
@ -135,7 +136,7 @@ namespace Yavsc.Blogs.Controllers
}
await blogSpotService.Delete(User, id);
return Ok(blog);
return Ok(blog.GetPayload());
}
/// <summary>

View file

@ -6,7 +6,7 @@ using static Yavsc.Constants;
namespace Yavsc.Blogs.Controllers
{
[Produces("application/json")]
[Route(APIPrefix + "/blogtags")]
[Route(APIPrefix + "/" + BlogTagPath )]
public class BlogTagsApiController : Controller
{
private readonly ApplicationDbContext _context;

View file

@ -8,7 +8,7 @@ using static Yavsc.Constants;
namespace Yavsc.Blogs.Controllers
{
[Produces("application/json")]
[Route(APIPrefix +"/circle")]
[Route(APIPrefix +"/" + CirclePath)]
public class CircleApiController : Controller
{
private readonly ApplicationDbContext _context;

View file

@ -11,7 +11,7 @@ namespace Yavsc.Blogs.Controllers
{
[Authorize]
[Produces("application/json")]
[Route(APIPrefix + "/blogcomments")]
[Route(APIPrefix + "/" + CommentsPath)]
public class CommentsApiController : Controller
{
private readonly ApplicationDbContext _context;

View file

@ -8,7 +8,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>
@ -18,4 +18,4 @@
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" />
</ItemGroup>
</Project>
</Project>

View file

@ -11,7 +11,7 @@
<RunSettingsFilePath>$(MSBuildProjectDirectory)\test.runsettings</RunSettingsFilePath>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>
@ -89,4 +89,4 @@
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" />
</ItemGroup>
</Project>
</Project>

View file

@ -70,7 +70,7 @@
<div class="actiongroup">
@if ((await AuthorizationService.AuthorizeAsync(User, post, new ReadPermission())).Succeeded)
{
<a asp-action="Details" asp-route-id="@((IBlogPost)post).Id" class="btn btn-light">Details</a>
<a asp-action="Details" asp-route-id="@post.Id" class="btn btn-light">Details</a>
}
else
{

View file

@ -9,7 +9,7 @@
<RepositoryUrl>https://github.com/pazof/yavsc</RepositoryUrl>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>
@ -54,4 +54,4 @@
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" />
</ItemGroup>
</Project>
</Project>

View file

@ -0,0 +1,22 @@
using Yavsc.Models.Blog;
public static class PayloadHelpers
{
public static object GetPayload(this BlogPost post)
{
return new
{
post.Id,
post.Title,
post.Article,
post.DateCreated,
post.UserCreated,
post.DateModified,
post.UserModified,
post.AuthorId,
ACL = post.GetACL(),
Tags = post.GetTags(),
post.IsPublished
};
}
}

View file

@ -85,7 +85,7 @@ namespace Yavsc.Models.Blog
public string[] GetTags()
{
return Tags.Select(t => t.Tag.Name).ToArray();
return Tags?.Select(t => t.Tag.Name).ToArray() ?? Array.Empty<string>();
}
[InverseProperty("Post")]
@ -106,6 +106,7 @@ namespace Yavsc.Models.Blog
[NotMapped]
public bool IsPublished { get; set; }
[JsonIgnore]
/// <summary>
/// Explicit interface implementation of
/// <see cref="IBlogPost.Author"/>. The underlying

View file

@ -1,14 +1,17 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Yavsc.Models.Relationship;
namespace Yavsc.Models.Blog
{
public partial class BlogTag
{
[JsonIgnore]
[ForeignKey("PostId")]
public virtual BlogPost Post { get; set; }
public long PostId { get; set; }
[JsonIgnore]
[ForeignKey("TagId")]
public virtual Tag Tag{ get; set; }
public long TagId { get; set; }

View file

@ -13,15 +13,17 @@ namespace Yavsc.Models.Blog
[YaStringLength(1024)]
public string Article { get; set; }
[ForeignKeyAttribute(nameof(ReceiverId))][JsonIgnore]
[JsonIgnore]
[ForeignKeyAttribute(nameof(ReceiverId))]
public virtual BlogPost Post { get; set; }
[Required]
public long ReceiverId { get; set; }
public bool Visible { get; set; }
[ForeignKeyAttribute("AuthorId")][JsonIgnore]
[ForeignKeyAttribute("AuthorId")]
[JsonIgnore]
public virtual ApplicationUser Author {
get; set;
}

View file

@ -9,7 +9,7 @@
<Library>true</Library>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>
@ -43,4 +43,4 @@
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" />
</ItemGroup>
</Project>
</Project>

View file

@ -0,0 +1,31 @@
namespace Yavsc.Blogs.Tests.Fixtures;
using static Yavsc.Constants;
public static class BlogHelpers
{
public static string ApiUrl(this IBackendFixture fixture, string apiSubPath)
{
var secured = fixture.Addresses.FirstOrDefault(a => a.StartsWith("https://"));
if (secured is null)
{
var unsecured = fixture.Addresses.FirstOrDefault(a => a.StartsWith("http://"));
if (unsecured is null)
{
throw new InvalidOperationException("No backend address found");
}
return $"{unsecured}/{APIPrefix}/{apiSubPath}";
}
return $"{secured}/{APIPrefix}/{apiSubPath}";
}
public static string BlogAclUrl(this IBackendFixture fixture)
=> fixture.ApiUrl(BlogAclPath);
public static string BlogSpotUrl(this IBackendFixture fixture)
=> fixture.ApiUrl(BlogSpotPath);
public static string PublishUrl(this IBackendFixture fixture, long id)
=> fixture.ApiUrl(BlogSpotPath) +"/" + id + "/publish";
}

View file

@ -0,0 +1,13 @@
public interface IBackendFixture
{
/// <summary>
/// The addresses the fixture bound to.
/// </summary>
IReadOnlyList<string> Addresses { get; }
/// <summary>
/// The service provider for the fixture host.
/// </summary>
IServiceProvider Services { get; }
}

View file

@ -30,7 +30,7 @@ namespace Yavsc.Tests.Shared;
/// mapping are the responsibility of the subclass, through
/// <see cref="BuildApp"/>.
/// </summary>
public abstract class WebHostFixture : IDisposable
public abstract class WebHostFixture : IDisposable, IBackendFixture
{
private static readonly Lazy<X509Certificate2> _selfSignedCertificate =
new Lazy<X509Certificate2>(CreateSelfSignedCertificate);

View file

@ -14,7 +14,7 @@
-->
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>
@ -23,4 +23,7 @@
<PackageReference Include="Microsoft.IdentityModel.Tokens" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Yavsc.Abstract/Yavsc.Abstract.csproj" />
</ItemGroup>
</Project>

View file

@ -7,7 +7,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>