using System.Net; using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Yavsc.Models; using Yavsc.Models.Blog; using Yavsc.Tests.Shared; namespace Yavsc.Blogs.Tests; /// /// Behavioural tests for the publication toggle endpoint: /// PUT /api/BlogApi/{id}/publish with body /// { "publish": bool }. /// /// The endpoint is the PostIt-facing way to toggle /// whether a post is publicly readable (via /// BlogSpotPublication). It does NOT change the /// ACL — a Public post with a non-empty ACL is still /// restricted to the ACL's circles for authenticated /// callers; only anonymous reads open up. /// /// Same fixture as : /// in-memory ApplicationDbContext, JWT bearer auth /// via . /// [Collection("JwtClaimMapping")] public sealed class PublishEndpointTests : IClassFixture { private readonly BlogsWebServerFixture _fixture; public PublishEndpointTests(BlogsWebServerFixture fixture) { _fixture = fixture; } private void ResetDatabase() { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); db.Database.EnsureDeleted(); db.Database.EnsureCreated(); // ApplicationUser has an AlternateKey on Email; the // InMemory provider refuses to track entities whose // alternate key is null, so we set it explicitly. db.Users.Add(new ApplicationUser { Id = "alice", UserName = "alice", Email = "alice@example.com", EmailConfirmed = true, }); db.SaveChanges(); } private long SeedPost(string authorId) { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var post = new BlogPost { AuthorId = authorId, Title = $"post-by-{authorId}", Article = "test", DateCreated = DateTime.UtcNow, DateModified = DateTime.UtcNow, }; db.BlogSpot.Add(post); db.SaveChanges(); 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 { ServerCertificateCustomValidationCallback = (_, _, _, _) => true }; var http = new HttpClient(handler) { BaseAddress = new Uri(_fixture.Addresses.First(a => a.StartsWith("https://"))) }; http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue( "Bearer", TestTokenIssuer.Issue(subject)); return http; } [Fact] public async Task PutPublish_true_returns_204_and_sets_IsPublished_in_subsequent_GET() { ResetDatabase(); var postId = SeedPost("alice"); using var http = NewClient("alice"); var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }); Assert.Equal(HttpStatusCode.NoContent, put.StatusCode); var get = await http.GetAsync($"{BlogsUrl}/{postId}"); Assert.Equal(HttpStatusCode.OK, get.StatusCode); using var doc = JsonDocument.Parse(await get.Content.ReadAsStringAsync()); Assert.True(doc.RootElement.GetProperty("isPublished").GetBoolean()); } [Fact] public async Task PutPublish_false_returns_204_and_clears_IsPublished() { ResetDatabase(); var postId = SeedPost("alice"); using var http = NewClient("alice"); await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }); var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = false }); Assert.Equal(HttpStatusCode.NoContent, put.StatusCode); var get = await http.GetAsync($"{BlogsUrl}/{postId}"); using var doc = JsonDocument.Parse(await get.Content.ReadAsStringAsync()); Assert.False(doc.RootElement.GetProperty("isPublished").GetBoolean()); } [Fact] public async Task PutPublish_on_unknown_post_returns_404() { ResetDatabase(); using var http = NewClient("alice"); var put = await http.PutAsJsonAsync(PublishUrl(99999L), new { publish = true }); Assert.Equal(HttpStatusCode.NotFound, put.StatusCode); } [Fact] public async Task PutPublish_by_non_author_returns_challenge() { ResetDatabase(); var postId = SeedPost("alice"); using var http = NewClient("bob"); var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }); // 401 Challenge (the controller returns Challenge() // for AuthorizationFailureException). The exact code // is framework-dependent; what matters is "not 204". Assert.NotEqual(HttpStatusCode.NoContent, put.StatusCode); } }