Merge pull request 'release/1.0.8-rc4' (#44) from release/1.0.8-rc4 into main
Reviewed-on: #44
This commit is contained in:
commit
4747942f93
39 changed files with 343 additions and 217 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -42,5 +42,6 @@
|
|||
"copilotcli/gpt-5.3-codex"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"dotnet.defaultSolution": "yavsc.sln"
|
||||
}
|
||||
|
|
|
|||
32
.vscode/tasks.json
vendored
32
.vscode/tasks.json
vendored
|
|
@ -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
|
||||
},
|
||||
{
|
||||
|
|
|
|||
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:<random></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 <jwt></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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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".
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
22
src/Yavsc.Server/Helpers/PayloadHelpers.cs
Normal file
22
src/Yavsc.Server/Helpers/PayloadHelpers.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
31
src/Yavsc.Tests.Shared/BlogHelpers.cs
Normal file
31
src/Yavsc.Tests.Shared/BlogHelpers.cs
Normal 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";
|
||||
|
||||
}
|
||||
13
src/Yavsc.Tests.Shared/IBackendFixture.cs
Normal file
13
src/Yavsc.Tests.Shared/IBackendFixture.cs
Normal 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; }
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue