diff --git a/MyFirstBlog/Controllers/PostsController.cs b/MyFirstBlog/Controllers/PostsController.cs index 8fa6bf2c..70834a5d 100644 --- a/MyFirstBlog/Controllers/PostsController.cs +++ b/MyFirstBlog/Controllers/PostsController.cs @@ -1,34 +1,34 @@ -namespace MyFirstBlog.Controllers; - using Microsoft.AspNetCore.Mvc; using MyFirstBlog.Dtos; using MyFirstBlog.Services; - -[ApiController] -[Route("posts")] - -public class PostsController : ControllerBase { - private IPostService _postService; - - public PostsController(IPostService postService) { - _postService = postService; - } - - // Get /posts - [HttpGet] - public IEnumerable GetPosts() { - return _postService.GetPosts(); - } - - // Get /posts/:slug - [HttpGet("{slug}")] - public ActionResult GetPost(string slug) { - var post = _postService.GetPost(slug); - - if (post is null) { - return NotFound(); +using System.Collections.Generic; + +namespace MyFirstBlog.Controllers +{ + [ApiController] + [Route("posts")] + public class PostsController : ControllerBase + { + private readonly PostService _postService; + + public PostsController(PostService postService) + { + _postService = postService; } - return post; + [HttpPost] + public IActionResult CreatePost([FromBody] CreatePostDto dto) + { + if (string.IsNullOrWhiteSpace(dto.Title)) + { + return BadRequest(new Dictionary> + { + { "errors", new List { "Title cannot be blank" } } + }); + } + + var post = _postService.CreatePost(dto); + return Created("", new Dictionary { { "post", post } }); + } } } diff --git a/MyFirstBlog/Dtos/CreatePostDto.cs b/MyFirstBlog/Dtos/CreatePostDto.cs new file mode 100644 index 00000000..44cf2cdb --- /dev/null +++ b/MyFirstBlog/Dtos/CreatePostDto.cs @@ -0,0 +1,10 @@ +namespace MyFirstBlog.Dtos; + +public record CreatePostDto +{ + public Guid Id { get; init; } + public string Title { get; init; } = default!; + public string Slug { get; init; } = default!; + public string Description { get; init; } = default!; + public DateTime CreatedDate { get; init; } +} diff --git a/MyFirstBlog/Dtos/PostDto.cs b/MyFirstBlog/Dtos/PostDto.cs index 12ca4818..bcf1aff7 100644 --- a/MyFirstBlog/Dtos/PostDto.cs +++ b/MyFirstBlog/Dtos/PostDto.cs @@ -1,9 +1,10 @@ namespace MyFirstBlog.Dtos; -public record PostDto { +public record PostDto +{ public Guid Id { get; init; } public string Title { get; init; } = default!; public string Slug { get; init; } = default!; - public string Body { get; init; } = default!; + public string Description { get; init; } = default!; public DateTime CreatedDate { get; init; } } diff --git a/MyFirstBlog/Entities/Post.cs b/MyFirstBlog/Entities/Post.cs index 9347e859..eabcbbd5 100644 --- a/MyFirstBlog/Entities/Post.cs +++ b/MyFirstBlog/Entities/Post.cs @@ -3,6 +3,5 @@ public record Post { public Guid Id { get; init; } public string Title { get; init; } = default!; public string Slug { get; init; } = default!; - public string Body { get; init; } = default!; public DateTime CreatedDate { get; init; } } diff --git a/MyFirstBlog/Extensions.cs b/MyFirstBlog/Extensions.cs index a5326018..5b58598b 100644 --- a/MyFirstBlog/Extensions.cs +++ b/MyFirstBlog/Extensions.cs @@ -9,7 +9,6 @@ public static PostDto AsDto(this Post post) { Id = post.Id, Title = post.Title, Slug = post.Slug, - Body = post.Body, CreatedDate = post.CreatedDate }; diff --git a/MyFirstBlog/MyFirstBlog.csproj b/MyFirstBlog/MyFirstBlog.csproj index 04ccc9c1..ec42b287 100644 --- a/MyFirstBlog/MyFirstBlog.csproj +++ b/MyFirstBlog/MyFirstBlog.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 Linux enable @@ -14,8 +14,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + + diff --git a/MyFirstBlog/Program.cs b/MyFirstBlog/Program.cs index 4042b610..99f2b2e0 100644 --- a/MyFirstBlog/Program.cs +++ b/MyFirstBlog/Program.cs @@ -1,48 +1,33 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using MyFirstBlog.Helpers; using MyFirstBlog.Services; - -var MyAllowLocalhostOrigins = "_myAllowLocalhostOrigins"; +using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); -var services = builder.Services; -var env = builder.Environment; - -// Add services to the container. - -services.AddDbContext(); - -services.AddCors(policyBuilder => { - policyBuilder.AddPolicy( MyAllowLocalhostOrigins, - policy => { - policy.WithOrigins("http://localhost:3000").AllowAnyHeader().AllowAnyMethod(); - }); -}); +// Use in-memory database +// builder.Services.AddDbContext(options => + // options.UseInMemoryDatabase("MyFirstBlogDummyDb")); -services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -services.AddEndpointsApiExplorer(); -services.AddSwaggerGen(); +// Register services +builder.Services.AddScoped(); -services.AddScoped(); +// Add controllers and Swagger +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); var app = builder.Build(); -var scope = app.Services.CreateScope(); -await DatabaseHelper.ManageMigrationsAsync(scope.ServiceProvider); +// No migrations needed for in-memory DB -// Configure the HTTP request pipeline. -if (env.IsDevelopment()) +// Middleware pipeline +if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); - - app.UseCors(MyAllowLocalhostOrigins); -} - -if (env.IsProduction()) -{ - app.UseHttpsRedirection(); } app.UseAuthorization(); diff --git a/MyFirstBlog/Services/IPostService.cs b/MyFirstBlog/Services/IPostService.cs new file mode 100644 index 00000000..bf3a4102 --- /dev/null +++ b/MyFirstBlog/Services/IPostService.cs @@ -0,0 +1,9 @@ +using MyFirstBlog.Dtos; + +namespace MyFirstBlog.Services +{ + public interface IPostService + { + PostDto CreatePost(CreatePostDto dto); + } +} diff --git a/MyFirstBlog/Services/PostService.cs b/MyFirstBlog/Services/PostService.cs index 6bac099f..dbabefce 100644 --- a/MyFirstBlog/Services/PostService.cs +++ b/MyFirstBlog/Services/PostService.cs @@ -1,37 +1,19 @@ -namespace MyFirstBlog.Services; - -using MyFirstBlog.Helpers; -using MyFirstBlog.Entities; -using System.Text.RegularExpressions; using MyFirstBlog.Dtos; -public interface IPostService -{ - IEnumerable GetPosts(); - PostDto GetPost(String slug); -} - -public class PostService : IPostService +namespace MyFirstBlog.Services { - private DataContext _context; - - public PostService(DataContext context) - { - _context = context; - } - - public IEnumerable GetPosts() - { - return _context.Posts.Select(post => post.AsDto()); - } - - public PostDto GetPost(string slug) - { - return getPost(slug).AsDto(); - } - - private Post getPost(string slug) + public class PostService : IPostService { - return _context.Posts.Where(a=>a.Slug==slug.ToString()).SingleOrDefault(); + public PostDto CreatePost(CreatePostDto dto) + { + return new PostDto + { + Id = dto.Id != Guid.Empty ? dto.Id : Guid.NewGuid(), + Title = dto.Title, + Slug = dto.Slug, + Description = dto.Description, // <-- changed from Body to Description + CreatedDate = dto.CreatedDate != default ? dto.CreatedDate : DateTime.UtcNow + }; + } } } diff --git a/MyFirstBlogTests/MyFirstBlogTests.csproj b/MyFirstBlogTests/MyFirstBlogTests.csproj index 79ed3371..d92a3ea5 100644 --- a/MyFirstBlogTests/MyFirstBlogTests.csproj +++ b/MyFirstBlogTests/MyFirstBlogTests.csproj @@ -1,16 +1,25 @@ - - net5.0 + + net8.0 + false + - false - + + + + - - - - - - + + + + + + + + + + + diff --git a/MyFirstBlogTests/MyFirstBlogTests.csproj # Edit b/MyFirstBlogTests/MyFirstBlogTests.csproj # Edit new file mode 100644 index 00000000..92435047 --- /dev/null +++ b/MyFirstBlogTests/MyFirstBlogTests.csproj # Edit @@ -0,0 +1,25 @@ + + + + net8.0 + false + + + + + + + + + + + + + + + + + + + + diff --git a/MyFirstBlogTests/PostControllerTests.cs b/MyFirstBlogTests/PostControllerTests.cs new file mode 100644 index 00000000..4f69fd61 --- /dev/null +++ b/MyFirstBlogTests/PostControllerTests.cs @@ -0,0 +1,55 @@ +// MyFirstBlogTests/PostControllerTests.cs + +using Xunit; +using MyFirstBlog.Controllers; +using MyFirstBlog.Services; +using MyFirstBlog.Dtos; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; + +namespace MyFirstBlogTests +{ + public class PostControllerTests + { + private readonly PostsController _controller; + + public PostControllerTests() + { + _controller = new PostsController(new PostService()); + } + + [Fact] + public void CreatePost_ValidInput_ReturnsCreated() + { + var postDto = new CreatePostDto + { + Title = "Test Title", + Description = "Test Description" + }; + + var result = _controller.CreatePost(postDto) as CreatedResult; + Assert.NotNull(result); + + var post = result.Value as PostDto; + Assert.Equal("Test Title", post.Title); + Assert.Equal("Test Description", post.Description); + } + + [Fact] + public void CreatePost_EmptyTitle_ReturnsBadRequest() + { + var postDto = new CreatePostDto + { + Title = "", + Description = "Some content" + }; + + var result = _controller.CreatePost(postDto) as BadRequestObjectResult; + Assert.NotNull(result); + + var errors = result.Value as Dictionary>; + Assert.True(errors.ContainsKey("errors")); + Assert.Contains("Title cannot be blank", errors["errors"]); + } + } +} diff --git a/MyFirstBlogTests/PostServiceTests.cs b/MyFirstBlogTests/PostServiceTests.cs new file mode 100644 index 00000000..4804e9c5 --- /dev/null +++ b/MyFirstBlogTests/PostServiceTests.cs @@ -0,0 +1,44 @@ +using Xunit; +using MyFirstBlog.Services; +using MyFirstBlog.Dtos; +using System.ComponentModel.DataAnnotations; +using System; + +namespace MyFirstBlog.Tests +{ + public class PostServiceTests + { + [Fact] + public void CreatePost_WithValidData_ReturnsPost() + { + var service = new PostService(); + + var dto = new CreatePostDto + { + Title = "Hello", + Description = "World" + }; + + var result = service.CreatePost(dto); + + Assert.NotNull(result); + Assert.Equal("Hello", result.Title); + Assert.Equal("World", result.Description); + } + + [Fact] + public void CreatePost_WithBlankTitle_ThrowsValidationException() + { + var service = new PostService(); + + var dto = new CreatePostDto + { + Title = "", + Description = "World" + }; + + var ex = Assert.Throws(() => service.CreatePost(dto)); + Assert.Contains("Title cannot be blank", ex.Message); + } + } +} diff --git a/MyFirstBlogTests/UnitTest1.cs b/MyFirstBlogTests/UnitTest1.cs index 921a6668..edf47dea 100644 --- a/MyFirstBlogTests/UnitTest1.cs +++ b/MyFirstBlogTests/UnitTest1.cs @@ -1,18 +1,62 @@ -using NUnit.Framework; +using Xunit; +using MyFirstBlog.Controllers; +using MyFirstBlog.Services; +using MyFirstBlog.Dtos; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System; namespace MyFirstBlogTests { - public class Tests + public class UnitTest1 { - [SetUp] - public void Setup() + private readonly PostsController _controller; + + public UnitTest1() { + _controller = new PostsController(new PostService()); } - [Test] - public void Test1() + [Fact] + public void CreatePost_WithValidInput_ReturnsCreated() { - Assert.Pass(); + var input = new CreatePostDto + { + Id = Guid.NewGuid(), + Title = "Sample Title", + Slug = "sample-title", + Description = "Some blog post content", + CreatedDate = DateTime.UtcNow + }; + + var result = _controller.CreatePost(input) as CreatedResult; + Assert.NotNull(result); + + var response = result.Value as Dictionary; + Assert.NotNull(response); + + var post = response["post"]; + Assert.Equal("Sample Title", post.Title); + Assert.Equal("sample-title", post.Slug); + Assert.Equal("Some blog post content", post.Description); // ✅ Updated + } + + [Fact] + public void CreatePost_WithBlankTitle_ReturnsBadRequest() + { + var input = new CreatePostDto + { + Title = "", + Slug = "no-title", + Description = "No title content", + CreatedDate = DateTime.UtcNow + }; + + var result = _controller.CreatePost(input) as BadRequestObjectResult; + Assert.NotNull(result); + + var errors = result.Value as Dictionary>; + Assert.Contains("Title cannot be blank", errors["errors"]); } } } diff --git a/Screenshot 2025-08-03 184016.png b/Screenshot 2025-08-03 184016.png new file mode 100644 index 00000000..e06f499a Binary files /dev/null and b/Screenshot 2025-08-03 184016.png differ