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;
namespace Yavsc.Blogs.Tests;
///
/// Behavioural tests for BlogAclApiController.PostCircleAuthorizationToBlogPost:
/// POST /api/v1/blogacl with a JSON body of
/// CircleAuthorizationToBlogPost (CircleId + BlogPostId).
///
/// Same fixture as :
/// provides a SQLite
/// :memory:ApplicationDbContext (so FKs are
/// enforced the way a real relational engine would) and JWT
/// bearer auth via TestTokenIssuer. No mocks — the real
/// DbContext receives the real INSERT attempt.
///
/// The bug being pinned by these tests: the POST endpoint
/// calls _context.CircleAuthorizationToBlogPost.Add(...)
/// then SaveChangesAsync. The entity has a composite
/// key (CircleId + BlogPostId) and two FKs; EF Core refuses
/// the INSERT with
/// System.InvalidOperationException: The value of
/// 'CircleAuthorizationToBlogPost.BlogPostId' is unknown when
/// attempting to save changes when the principal entities
/// (the existing BlogPost and Circle) are not
/// attached to the DbContext in the same change-tracker graph.
///
[Collection("Yavsc Blogs")]
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}/{BlogAclPath}";
/// Delete any ACL rows tied to the fixture's seeded
/// (CircleId, BlogPostId) pair. The shared SQLite store
/// persists across tests, so tests that POST a successful ACL
/// row would otherwise conflict with whichever other test runs
/// next against the same pair — xUnit does not guarantee
/// execution order. Calling this at the start of each
/// insert-bearing test guarantees a clean slate regardless of
/// the previous test's outcome.
private void CleanupAcl()
{
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
db.CircleAuthorizationToBlogPost
.Where(a => a.CircleId == _fixture.CircleId
&& a.BlogPostId == _fixture.PostId)
.ExecuteDelete();
}
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://")))
};
// The Blogs fixture disables JwtSecurityTokenHandler's
// inbound claim-type remap, so the JWT's "sub" stays "sub"
// rather than being rewritten to ClaimTypes.NameIdentifier.
// The controller, however, reads the user id via
// User.FindFirstValue(ClaimTypes.NameIdentifier), so we add
// an explicit nameid claim to keep the legacy lookup happy.
http.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"Bearer",
TestTokenIssuer.Issue(
subject,
extraClaims: new[]
{
new System.Security.Claims.Claim(
System.Security.Claims.ClaimTypes.NameIdentifier,
subject),
}));
return http;
}
///
/// Reproduces the prod 500 logged on 2026-08-21 on mercure:
/// InvalidOperationException: The value of
/// 'CircleAuthorizationToBlogPost.BlogPostId' is unknown
/// when POSTs the
/// shape { "circleId": <id> } — the exact body the
/// PostIt client builds from
/// (which only carries CircleId). The server deserialises
/// it into , leaves
/// BlogPostId at its default(long) = 0, attaches
/// no Target navigation, and EF Core refuses to INSERT
/// during PrepareToSave(). The fix lives in PostIt
/// (enrich the payload with blogPostId + comment)
/// and on the wire DTO ( must
/// carry those fields); the server validates. Until that ships,
/// this test stays red.
///
[Fact]
public async Task PostCircleAuthorization_returns_201_when_payload_mirrors_PostIt_shape_against_existing_circle_named_test()
{
// The prod circle already exists with Name="test", Public=true,
// 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(_fixture.DefaultUserLogin);
var payload = new PostAccessControlRulePayload
{
CircleId = _fixture.CircleId,
BlogPostId = _fixture.PostId
};
var response = await http.PostAsJsonAsync(BlogAclUrl(), payload,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
///
/// Payload templates for .
/// Each row carries the shape we want to POST; -1L and
/// -2L are negative sentinels that the test substitutes
/// with the ids of freshly seeded Circle / BlogPost
/// rows before sending, so every shape lands against a real
/// principal entity and the seeded fixtures are not dead.
///
public static IEnumerable