diff --git a/src/Yavsc.Org.Tests/Controllers/CommentsApiIntegrationTests.cs b/src/Yavsc.Org.Tests/Controllers/CommentsApiIntegrationTests.cs new file mode 100644 index 00000000..4b145cd7 --- /dev/null +++ b/src/Yavsc.Org.Tests/Controllers/CommentsApiIntegrationTests.cs @@ -0,0 +1,92 @@ +using System.Net; +using System.Net.Http.Json; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Yavsc.Models; +using Yavsc.Models.Blog; +using Yavsc.Tests.Shared; + +namespace Yavsc.Org.Tests.Controllers; + +public class CommentsApiIntegrationTests : IClassFixture +{ + private readonly TestWebApplicationFactory _factory; + + public CommentsApiIntegrationTests(TestWebApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task Post_blogcomments_json_returns_201_and_persists_comment() + { + long postId; + + using (var scope = _factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + + if (!db.Users.Any(u => u.Id == TestUserMiddleware.UserId)) + { + db.Users.Add(new ApplicationUser + { + Id = TestUserMiddleware.UserId, + UserName = "test-user", + NormalizedUserName = "TEST-USER", + Email = "test-user@example.com", + NormalizedEmail = "TEST-USER@EXAMPLE.COM", + EmailConfirmed = true, + SecurityStamp = Guid.NewGuid().ToString("N"), + ConcurrencyStamp = Guid.NewGuid().ToString("N") + }); + } + + var post = new BlogPost + { + Title = "Post for comment API test", + AuthorId = TestUserMiddleware.UserId, + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow + }; + + db.BlogSpot.Add(post); + await db.SaveChangesAsync(TestContext.Current.CancellationToken); + postId = post.Id; + } + + var http = _factory.CreateClient(new WebApplicationFactoryClientOptions + { + HandleCookies = true, + AllowAutoRedirect = false + }); + http.DefaultRequestHeaders.Add(TestAuthPolicyProvider.HeaderName, TestAuthPolicyProvider.AdminRole); + + var response = await http.PostAsJsonAsync( + "/api/v1/blogcomments", + new + { + Article = "Comment API integration test", + ReceiverId = postId + }, + TestContext.Current.CancellationToken); + var responseBody = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + Assert.True( + response.StatusCode != HttpStatusCode.InternalServerError, + $"Unexpected 500 on POST /api/v1/blogcomments. Body: {responseBody}"); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.Contains("\"id\"", responseBody, StringComparison.OrdinalIgnoreCase); + Assert.Contains("\"dateCreated\"", responseBody, StringComparison.OrdinalIgnoreCase); + + using var verifyScope = _factory.Services.CreateScope(); + var verifyDb = verifyScope.ServiceProvider.GetRequiredService(); + var stored = await verifyDb.Comment + .OrderByDescending(c => c.Id) + .FirstOrDefaultAsync(c => c.ReceiverId == postId, TestContext.Current.CancellationToken); + + Assert.NotNull(stored); + Assert.Equal("Comment API integration test", stored!.Article); + Assert.Equal(TestUserMiddleware.UserId, stored.AuthorId); + } +} diff --git a/src/Yavsc.Org.Tests/Controllers/CommentsControllerTests.cs b/src/Yavsc.Org.Tests/Controllers/CommentsControllerTests.cs new file mode 100644 index 00000000..28213eff --- /dev/null +++ b/src/Yavsc.Org.Tests/Controllers/CommentsControllerTests.cs @@ -0,0 +1,63 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Yavsc.Controllers; +using Yavsc.Models; +using Yavsc.Models.Blog; + +namespace Yavsc.Org.Tests.Controllers; + +public class CommentsControllerTests +{ + [Fact] + public async Task Create_sets_author_and_persists_comment() + { + var dbName = $"comments-controller-{Guid.NewGuid():N}"; + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(dbName) + .Options; + + await using var db = new ApplicationDbContext(options); + var post = new BlogPost + { + Title = "Post de test", + AuthorId = "post-author", + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow + }; + db.BlogSpot.Add(post); + await db.SaveChangesAsync(TestContext.Current.CancellationToken); + + var controller = new CommentsController(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [ + new Claim(ClaimTypes.NameIdentifier, "comment-author") + ], "TestAuth")) + } + } + }; + + var comment = new Comment + { + ReceiverId = post.Id, + Article = "Commentaire de test", + Visible = true + }; + + var result = await controller.Create(comment); + + var redirect = Assert.IsType(result); + Assert.Equal("Index", redirect.ActionName); + + var stored = await db.Comment.SingleAsync(TestContext.Current.CancellationToken); + Assert.Equal("comment-author", stored.AuthorId); + Assert.Equal(post.Id, stored.ReceiverId); + Assert.Equal("Commentaire de test", stored.Article); + } +} diff --git a/src/Yavsc.Org.Tests/TestUserStartupFilter.cs b/src/Yavsc.Org.Tests/TestUserStartupFilter.cs index 53439ed9..06c6caa8 100644 --- a/src/Yavsc.Org.Tests/TestUserStartupFilter.cs +++ b/src/Yavsc.Org.Tests/TestUserStartupFilter.cs @@ -33,15 +33,11 @@ public class TestUserStartupFilter : IStartupFilter { return app => { - // Replay the production pipeline first (this is what - // Program.Main + ConfigurePipeline set up, including - // UseAuthentication and UseAuthorization). - next(app); - // Then add our middleware on top. UseMiddleware wires - // it through the same IMiddlewareActivator the framework - // uses, so the dependency on TestUserMiddleware is - // resolved from the request scope. app.UseMiddleware(); + // Replay the production pipeline after the test middleware, + // so downstream auth and controllers can see the injected + // principal when no real login flow is used. + next(app); }; } } diff --git a/src/Yavsc.Org/Controllers/Communicating/CommentsController.cs b/src/Yavsc.Org/Controllers/Communicating/CommentsController.cs index f14a234f..3690671c 100644 --- a/src/Yavsc.Org/Controllers/Communicating/CommentsController.cs +++ b/src/Yavsc.Org/Controllers/Communicating/CommentsController.cs @@ -2,16 +2,17 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; -using Yavsc.Helpers; using Yavsc.Models; using Yavsc.Models.Blog; using Yavsc.Server.Helpers; namespace Yavsc.Controllers { + /// /// Comment some post. /// + [Route("~/api/v1/blogcomments")] public class CommentsController : Controller { private readonly ApplicationDbContext _context; @@ -21,7 +22,68 @@ namespace Yavsc.Controllers _context = context; } + [HttpGet("{id:long}", Name = "GetComment")] + public async Task GetComment(long id) + { + var comment = await _context.Comment.SingleOrDefaultAsync(m => m.Id == id); + if (comment == null) + { + return NotFound(); + } + + return Ok(comment); + } + + [HttpPost] + [IgnoreAntiforgeryToken] + [Consumes("application/json")] + public async Task Post([FromBody] CommentPost post) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var uid = User.GetUserId(); + if (string.IsNullOrEmpty(uid)) + { + return Challenge(); + } + + var article = await _context.BlogSpot.FirstOrDefaultAsync(p => p.Id == post.ReceiverId); + if (article == null) + { + ModelState.AddModelError(nameof(post.ReceiverId), "not found"); + return BadRequest(ModelState); + } + + if (post.ParentId != null) + { + var parentExists = await _context.Comment.AnyAsync(c => c.Id == post.ParentId); + if (!parentExists) + { + ModelState.AddModelError(nameof(post.ParentId), "not found"); + return BadRequest(ModelState); + } + } + + var comment = new Comment + { + ReceiverId = post.ReceiverId, + Article = post.Article, + ParentId = post.ParentId, + AuthorId = uid, + UserModified = uid + }; + + _context.Comment.Add(comment); + await _context.SaveChangesAsync(uid); + + return CreatedAtRoute("GetComment", new { id = comment.Id }, new { id = comment.Id, dateCreated = comment.DateCreated }); + } + // GET: Comments + [HttpGet] public async Task Index() { var applicationDbContext = _context.Comment.Include(c => c.Post); @@ -45,19 +107,24 @@ namespace Yavsc.Controllers return View(comment); } - // GET: Comments/Create + // GET: Comments/Create (MVC form endpoint) + [HttpGet("form")] public IActionResult Create() { - ViewBag.ReceiverId = new SelectList(_context.BlogSpot, "Id", "Post"); + ViewBag.ReceiverId = new SelectList(_context.BlogSpot, "Id", "Title"); return View(); } - // POST: Comments/Create - [HttpPost] + // POST: Comments/Create (MVC form endpoint) + [HttpPost("form")] [ValidateAntiForgeryToken] public async Task Create(Comment comment) { comment.UserCreated = User.GetUserId(); + // AuthorId/UserCreated is set server-side after model binding; + // remove the stale binding error so a valid authenticated POST + // does not fall into the invalid branch. + ModelState.Remove(nameof(Comment.AuthorId)); if (ModelState.IsValid) { @@ -65,7 +132,7 @@ namespace Yavsc.Controllers await _context.SaveChangesAsync(); return RedirectToAction("Index"); } - ViewBag.ReceiverId = new SelectList(_context.BlogSpot, "Id", "Post", comment.ReceiverId); + ViewBag.ReceiverId = new SelectList(_context.BlogSpot, "Id", "Title", comment.ReceiverId); return View(comment); } @@ -82,7 +149,7 @@ namespace Yavsc.Controllers { return NotFound(); } - ViewBag.ReceiverId = new SelectList(_context.BlogSpot, "Id", "Post", comment.ReceiverId); + ViewBag.ReceiverId = new SelectList(_context.BlogSpot, "Id", "Title", comment.ReceiverId); return View(comment); } @@ -97,7 +164,7 @@ namespace Yavsc.Controllers await _context.SaveChangesAsync(); return RedirectToAction("Index"); } - ViewBag.ReceiverId = new SelectList(_context.BlogSpot, "Id", "Post", comment.ReceiverId); + ViewBag.ReceiverId = new SelectList(_context.BlogSpot, "Id", "Title", comment.ReceiverId); return View(comment); } diff --git a/src/Yavsc.Org/Controllers/DimissClicksApiController.cs b/src/Yavsc.Org/Controllers/DimissClicksApiController.cs index bc1c10b6..19f90787 100644 --- a/src/Yavsc.Org/Controllers/DimissClicksApiController.cs +++ b/src/Yavsc.Org/Controllers/DimissClicksApiController.cs @@ -10,7 +10,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/dimiss")] + [Route("api/v1/dimiss")] public class DimissClicksApiController : Controller { private readonly ApplicationDbContext _context; diff --git a/src/Yavsc.Org/wwwroot/js/site.js b/src/Yavsc.Org/wwwroot/js/site.js index 562e497b..e1bfb3cf 100644 --- a/src/Yavsc.Org/wwwroot/js/site.js +++ b/src/Yavsc.Org/wwwroot/js/site.js @@ -3,7 +3,7 @@ var notifClick = function(nid) { if (nid > 0) { $.get({ - url: '/api/dimiss/click/' + nid, + url: '/api/v1/dimiss/click/' + nid, success: $('div[data-nid='+nid+']').remove() }); }