diff --git a/.vscode/settings.json b/.vscode/settings.json index 915683fc..1cb334e0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -42,5 +42,6 @@ "copilotcli/gpt-5.3-codex" ] } - } + }, + "dotnet.defaultSolution": "yavsc.sln" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3c0c0bf2..3faedae0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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 }, { diff --git a/CHANGELOG.md b/CHANGELOG.md index 6adf462a..8425c2c6 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index 62b494f8..7f44645d 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -9,23 +9,22 @@ 1.0 apk false + 1.1.0.0 + 1.1.0.0 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 + 1.1.0-beta.1 - Resources\drawable\Icon.png - - - - + - - + \ No newline at end of file diff --git a/src/PostIt/PostIt.Browser/PostIt.Browser.csproj b/src/PostIt/PostIt.Browser/PostIt.Browser.csproj index 2816b2f8..67600a72 100644 --- a/src/PostIt/PostIt.Browser/PostIt.Browser.csproj +++ b/src/PostIt/PostIt.Browser/PostIt.Browser.csproj @@ -6,7 +6,7 @@ enable 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 @@ -15,4 +15,4 @@ - + \ No newline at end of file diff --git a/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj b/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj index e47c5b6d..30ba65c4 100644 --- a/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj +++ b/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj @@ -7,7 +7,7 @@ enable 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 @@ -24,4 +24,4 @@ - + \ No newline at end of file diff --git a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj index 663bdd12..86a7998a 100644 --- a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj +++ b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj @@ -8,7 +8,7 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 @@ -16,9 +16,7 @@ - - @@ -28,6 +26,5 @@ - - - + + \ No newline at end of file diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj index bde8fd41..1ae36a44 100644 --- a/src/PostIt/PostIt/PostIt.csproj +++ b/src/PostIt/PostIt/PostIt.csproj @@ -1,14 +1,16 @@ - + net10.0 enable latest + 1.1.0.0 + 1.1.0.0 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 + 1.1.0-beta.1 - - PreserveNewest @@ -31,9 +33,8 @@ - - + \ No newline at end of file diff --git a/src/PostIt/PostIt/Services/UiDispatcher.cs b/src/PostIt/PostIt/Services/UiDispatcher.cs deleted file mode 100644 index e935ac1a..00000000 --- a/src/PostIt/PostIt/Services/UiDispatcher.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Threading.Tasks; -using Avalonia.Threading; - -namespace PostIt.Services; - -/// -/// Tiny marshalling helper around 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 -/// -derived types (Settings, the various -/// ViewModels) can fire PropertyChanged safely from background -/// work — which is exactly the cross-thread case that previously blew -/// up inside DataValidationErrors.SetErrors 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. Settings.Load continuation paths). -/// -public static class UiDispatcher -{ - /// - /// 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 . - /// - public static bool IsOnUiThread => Dispatcher.UIThread.CheckAccess(); - - /// - /// Run 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. - /// - 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 */ } - } - - /// - /// Fire-and-forget variant: schedules 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. - /// - public static void Post(Action action) - { - if (action is null) return; - try { Dispatcher.UIThread.Post(action, DispatcherPriority.Normal); } - catch (InvalidOperationException) { /* dispatcher gone */ } - } - - /// - /// Awaitable variant. Useful inside async ViewModel methods - /// that must touch bindings only after the dispatcher has processed - /// a queued update (e.g. "load file then refresh observable state"). - /// - 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(); - } -} diff --git a/src/Yavsc.Abstract/Constants.cs b/src/Yavsc.Abstract/Constants.cs index 78ff0838..a2649491 100644 --- a/src/Yavsc.Abstract/Constants.cs +++ b/src/Yavsc.Abstract/Constants.cs @@ -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"}, diff --git a/src/Yavsc.Abstract/Yavsc.Abstract.csproj b/src/Yavsc.Abstract/Yavsc.Abstract.csproj index ebbb2d1e..5094b14d 100644 --- a/src/Yavsc.Abstract/Yavsc.Abstract.csproj +++ b/src/Yavsc.Abstract/Yavsc.Abstract.csproj @@ -11,10 +11,10 @@ latest 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 - + \ No newline at end of file diff --git a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj index 047223a6..4c560f23 100644 --- a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj +++ b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj @@ -17,7 +17,7 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 @@ -26,4 +26,4 @@ - + \ No newline at end of file diff --git a/src/Yavsc.Api/Yavsc.Api.csproj b/src/Yavsc.Api/Yavsc.Api.csproj index 2ddeb72d..3655079b 100644 --- a/src/Yavsc.Api/Yavsc.Api.csproj +++ b/src/Yavsc.Api/Yavsc.Api.csproj @@ -7,7 +7,7 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 @@ -17,4 +17,4 @@ - + \ No newline at end of file diff --git a/src/Yavsc.Blogs.Tests/BlogAclApiTests.cs b/src/Yavsc.Blogs.Tests/BlogAclApiTests.cs index 92dd3b2c..4aef805f 100644 --- a/src/Yavsc.Blogs.Tests/BlogAclApiTests.cs +++ b/src/Yavsc.Blogs.Tests/BlogAclApiTests.cs @@ -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 { 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}"; /// Delete any ACL rows tied to the fixture's seeded /// (CircleId, BlogPostId) pair. The shared SQLite store @@ -120,7 +123,7 @@ public sealed class BlogAclApiTests : IClassFixture // 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 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 [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 ); } + + [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( + 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( + 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()); + } + } diff --git a/src/Yavsc.Blogs.Tests/BlogApiMappedClaimsTests.cs b/src/Yavsc.Blogs.Tests/BlogApiMappedClaimsTests.cs index f4878860..f6b46ba9 100644 --- a/src/Yavsc.Blogs.Tests/BlogApiMappedClaimsTests.cs +++ b/src/Yavsc.Blogs.Tests/BlogApiMappedClaimsTests.cs @@ -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 +public sealed class BlogApiMappedClaimsTests : +IClassFixture, +IBackendFixture { private readonly MappedClaimsBlogsWebServerFixture _fixture; + public IReadOnlyList 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(TestContext.Current.CancellationToken); @@ -93,7 +103,7 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture(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 _fixture = fixture; } - /// Reset the in-memory database to a known empty state. - /// UseInMemoryDatabase shares its store across the - /// lifetime of the instance, - /// so without a per-test reset the test order would leak - /// state between tests. - private void ResetDatabase() - { - using var scope = _fixture.Services.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - db.Database.EnsureDeleted(); - db.Database.EnsureCreated(); - } /// Reset the database and seed the /// tester row. Required @@ -55,18 +43,10 @@ public sealed class BlogApiTests : IClassFixture /// at SaveChanges and the controller returns 500. private void ResetAndSeedDefaultUser() { - ResetDatabase(); + _fixture.ResetDatabase(); _fixture.SeedUser("tester"); } - /// The fixture's WebApplication is bound to - /// https://localhost:<random> via - /// . We pick the first - /// https URL and append the controller route - /// (/api/v1/blog, matching the production - /// [Route(APIPrefix + "/blog")]). - private string BlogsUrl => - _fixture.Addresses.First(a => a.StartsWith("https://")) + "/api/v1/blog"; /// Build an authenticated client: a real /// Authorization: Bearer <jwt> header where the JWT @@ -111,10 +91,11 @@ public sealed class BlogApiTests : IClassFixture [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 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 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 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 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 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 [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 // 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 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 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 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( 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 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 // 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 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) diff --git a/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs b/src/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs similarity index 90% rename from src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs rename to src/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs index 218904ef..6e8ae31f 100644 --- a/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs +++ b/src/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs @@ -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; /// @@ -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; } +/// Reset the in-memory database to a known empty state. + /// UseInMemoryDatabase shares its store across the + /// lifetime of the instance, + /// so without a per-test reset the test order would leak + /// state between tests. + public void ResetDatabase() + { + using var scope = Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.EnsureDeleted(); + db.Database.EnsureCreated(); + } + public void CleanupAcl() + { + using var scope = Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + db.CircleAuthorizationToBlogPost + .Where(a => a.CircleId == CircleId + && a.BlogPostId == PostId) + .ExecuteDelete(); + } public override void Dispose() { @@ -310,7 +333,8 @@ public sealed class BlogsWebServerFixture : WebHostFixture /// Optional hook to fill in fields /// like FullName / Avatar / EmailConfirmed /// that downstream tests assert on. - public ApplicationUser SeedUser(string userName, Action? configure = null) + public ApplicationUser SeedUser(string userName, + Action? configure = null) { using var scope = Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); @@ -351,20 +375,31 @@ public sealed class BlogsWebServerFixture : WebHostFixture /// Create a circle owned by /// directly in the SQLite store and return its server-assigned /// id. - private long SeedCircle(string ownerId, string name, bool isPublic = false) + public long SeedCircle(string ownerId, string name, bool isPublic = false, + ICollection members = null + ) { using var scope = Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); 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; } /// Create a blog post owned by /// directly in the SQLite store and return its server-assigned /// id. - private long SeedBlogPost(string authorId, string title) + public long SeedBlogPost(string authorId, string title) { using var scope = Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); @@ -380,4 +415,5 @@ public sealed class BlogsWebServerFixture : WebHostFixture db.SaveChanges(); return post.Id; } + } diff --git a/src/Yavsc.Blogs.Tests/MappedClaimsBlogsWebServerFixture.cs b/src/Yavsc.Blogs.Tests/Fixtures/MappedClaimsBlogsWebServerFixture.cs similarity index 99% rename from src/Yavsc.Blogs.Tests/MappedClaimsBlogsWebServerFixture.cs rename to src/Yavsc.Blogs.Tests/Fixtures/MappedClaimsBlogsWebServerFixture.cs index 127f38fe..2546de0a 100644 --- a/src/Yavsc.Blogs.Tests/MappedClaimsBlogsWebServerFixture.cs +++ b/src/Yavsc.Blogs.Tests/Fixtures/MappedClaimsBlogsWebServerFixture.cs @@ -21,7 +21,7 @@ namespace Yavsc.Blogs.Tests; /// This is the closest in-process reproduction of the production /// authentication surface for the blog API. /// -public sealed class MappedClaimsBlogsWebServerFixture : IDisposable +public sealed class MappedClaimsBlogsWebServerFixture : IDisposable, IBackendFixture { private readonly InMemoryDatabaseRoot _inMemoryRoot = new(); private readonly Dictionary _savedInboundMap; diff --git a/src/Yavsc.Blogs.Tests/PublishEndpointTests.cs b/src/Yavsc.Blogs.Tests/PublishEndpointTests.cs index 7d5a02a9..113707ca 100644 --- a/src/Yavsc.Blogs.Tests/PublishEndpointTests.cs +++ b/src/Yavsc.Blogs.Tests/PublishEndpointTests.cs @@ -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 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 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 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 { 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 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". diff --git a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj index 34536806..bc4e34ba 100644 --- a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj +++ b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj @@ -9,7 +9,7 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 @@ -35,4 +35,4 @@ - + \ No newline at end of file diff --git a/src/Yavsc.Blogs/Constants.cs b/src/Yavsc.Blogs/Constants.cs index 3e499da4..9d400032 100644 --- a/src/Yavsc.Blogs/Constants.cs +++ b/src/Yavsc.Blogs/Constants.cs @@ -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"; diff --git a/src/Yavsc.Blogs/Controllers/BlogApiController.cs b/src/Yavsc.Blogs/Controllers/BlogApiController.cs index fcd3a336..8c76f7ee 100644 --- a/src/Yavsc.Blogs/Controllers/BlogApiController.cs +++ b/src/Yavsc.Blogs/Controllers/BlogApiController.cs @@ -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> 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 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 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()); } /// diff --git a/src/Yavsc.Blogs/Controllers/BlogTagsApiController.cs b/src/Yavsc.Blogs/Controllers/BlogTagsApiController.cs index ad6a0893..533e5594 100644 --- a/src/Yavsc.Blogs/Controllers/BlogTagsApiController.cs +++ b/src/Yavsc.Blogs/Controllers/BlogTagsApiController.cs @@ -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; diff --git a/src/Yavsc.Blogs/Controllers/CircleApiController.cs b/src/Yavsc.Blogs/Controllers/CircleApiController.cs index 524f0471..fafd00ac 100644 --- a/src/Yavsc.Blogs/Controllers/CircleApiController.cs +++ b/src/Yavsc.Blogs/Controllers/CircleApiController.cs @@ -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; diff --git a/src/Yavsc.Blogs/Controllers/CommentsApiController.cs b/src/Yavsc.Blogs/Controllers/CommentsApiController.cs index d4c80f99..d0747c29 100644 --- a/src/Yavsc.Blogs/Controllers/CommentsApiController.cs +++ b/src/Yavsc.Blogs/Controllers/CommentsApiController.cs @@ -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; diff --git a/src/Yavsc.Blogs/Yavsc.Blogs.csproj b/src/Yavsc.Blogs/Yavsc.Blogs.csproj index 019fb102..8692b28a 100644 --- a/src/Yavsc.Blogs/Yavsc.Blogs.csproj +++ b/src/Yavsc.Blogs/Yavsc.Blogs.csproj @@ -8,7 +8,7 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 @@ -18,4 +18,4 @@ - + \ No newline at end of file diff --git a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj index 34de5bd8..8019a78f 100644 --- a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj +++ b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj @@ -11,7 +11,7 @@ $(MSBuildProjectDirectory)\test.runsettings 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 @@ -89,4 +89,4 @@ - + \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Blogspot/Index.cshtml b/src/Yavsc.Org/Views/Blogspot/Index.cshtml index 52cf3b88..1582041c 100644 --- a/src/Yavsc.Org/Views/Blogspot/Index.cshtml +++ b/src/Yavsc.Org/Views/Blogspot/Index.cshtml @@ -70,7 +70,7 @@
@if ((await AuthorizationService.AuthorizeAsync(User, post, new ReadPermission())).Succeeded) { - Details + Details } else { diff --git a/src/Yavsc.Org/Yavsc.Org.csproj b/src/Yavsc.Org/Yavsc.Org.csproj index aeb94a8e..6dbceb4a 100644 --- a/src/Yavsc.Org/Yavsc.Org.csproj +++ b/src/Yavsc.Org/Yavsc.Org.csproj @@ -9,7 +9,7 @@ https://github.com/pazof/yavsc 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 @@ -54,4 +54,4 @@ - + \ No newline at end of file diff --git a/src/Yavsc.Server/Helpers/PayloadHelpers.cs b/src/Yavsc.Server/Helpers/PayloadHelpers.cs new file mode 100644 index 00000000..cd359bb4 --- /dev/null +++ b/src/Yavsc.Server/Helpers/PayloadHelpers.cs @@ -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 + }; + } +} diff --git a/src/Yavsc.Server/Models/Blog/BlogPost.cs b/src/Yavsc.Server/Models/Blog/BlogPost.cs index 5a2dc58d..e5fd615d 100644 --- a/src/Yavsc.Server/Models/Blog/BlogPost.cs +++ b/src/Yavsc.Server/Models/Blog/BlogPost.cs @@ -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(); } [InverseProperty("Post")] @@ -106,6 +106,7 @@ namespace Yavsc.Models.Blog [NotMapped] public bool IsPublished { get; set; } + [JsonIgnore] /// /// Explicit interface implementation of /// . The underlying diff --git a/src/Yavsc.Server/Models/Blog/BlogTag.cs b/src/Yavsc.Server/Models/Blog/BlogTag.cs index 69d428de..1f15a081 100644 --- a/src/Yavsc.Server/Models/Blog/BlogTag.cs +++ b/src/Yavsc.Server/Models/Blog/BlogTag.cs @@ -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; } diff --git a/src/Yavsc.Server/Models/Blog/Comment.cs b/src/Yavsc.Server/Models/Blog/Comment.cs index acb5e003..4d39d0c2 100644 --- a/src/Yavsc.Server/Models/Blog/Comment.cs +++ b/src/Yavsc.Server/Models/Blog/Comment.cs @@ -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; } diff --git a/src/Yavsc.Server/Yavsc.Server.csproj b/src/Yavsc.Server/Yavsc.Server.csproj index 7adb3a78..839e476a 100644 --- a/src/Yavsc.Server/Yavsc.Server.csproj +++ b/src/Yavsc.Server/Yavsc.Server.csproj @@ -9,7 +9,7 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 @@ -43,4 +43,4 @@ - + \ No newline at end of file diff --git a/src/Yavsc.Tests.Shared/BlogHelpers.cs b/src/Yavsc.Tests.Shared/BlogHelpers.cs new file mode 100644 index 00000000..ef5a7688 --- /dev/null +++ b/src/Yavsc.Tests.Shared/BlogHelpers.cs @@ -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"; + +} diff --git a/src/Yavsc.Tests.Shared/IBackendFixture.cs b/src/Yavsc.Tests.Shared/IBackendFixture.cs new file mode 100644 index 00000000..4e2f009d --- /dev/null +++ b/src/Yavsc.Tests.Shared/IBackendFixture.cs @@ -0,0 +1,13 @@ + +public interface IBackendFixture +{ + /// + /// The addresses the fixture bound to. + /// + IReadOnlyList Addresses { get; } + + /// + /// The service provider for the fixture host. + /// + IServiceProvider Services { get; } +} diff --git a/src/Yavsc.Tests.Shared/WebHostFixture.cs b/src/Yavsc.Tests.Shared/WebHostFixture.cs index 417d29d8..95b23082 100644 --- a/src/Yavsc.Tests.Shared/WebHostFixture.cs +++ b/src/Yavsc.Tests.Shared/WebHostFixture.cs @@ -30,7 +30,7 @@ namespace Yavsc.Tests.Shared; /// mapping are the responsibility of the subclass, through /// . /// -public abstract class WebHostFixture : IDisposable +public abstract class WebHostFixture : IDisposable, IBackendFixture { private static readonly Lazy _selfSignedCertificate = new Lazy(CreateSelfSignedCertificate); diff --git a/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj b/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj index 7f2f382a..6e7a4c5a 100644 --- a/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj +++ b/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj @@ -14,7 +14,7 @@ --> 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 @@ -23,4 +23,7 @@ + + + diff --git a/src/cli/cli.csproj b/src/cli/cli.csproj index 237ccdd8..49aa64b1 100644 --- a/src/cli/cli.csproj +++ b/src/cli/cli.csproj @@ -7,7 +7,7 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1