From f3cabd3b698f1e952dcf49904cb4f947a3495072 Mon Sep 17 00:00:00 2001 From: VV Date: Wed, 17 Jul 2024 19:35:55 +1200 Subject: [PATCH] Chirag Assessent --- Backend/TodoList.Api/Models/Models.csproj | 9 ++ Backend/TodoList.Api/Models/TodoItem.cs | 15 ++ .../Repository/ITodoRepository.cs | 19 +++ .../TodoList.Api/Repository/Repository.csproj | 9 ++ .../TodoList.Api.UnitTests/DummyTestShould.cs | 56 ++++++- .../TodoList.Api.UnitTests.csproj | 1 + Backend/TodoList.Api/TodoList.Api.sln | 18 ++- .../Controllers/TodoItemsController.cs | 143 +++++++++++------ .../Properties/launchSettings.json | 8 +- Backend/TodoList.Api/TodoList.Api/Startup.cs | 34 ++-- .../TodoList.Api/TodoList.Api.csproj | 9 ++ .../TodoList.Api/appsettings.json | 3 + .../TodoList.Api/TodoList.Models/TodoItem.cs | 15 ++ .../TodoList.Models/TodoList.Models.csproj | 9 ++ .../Interface/ITodoRepository.cs | 53 ++++++ .../Service/TodoRepository.cs | 84 ++++++++++ .../TodoContext.cs | 11 +- .../TodoList.RepositoryService.csproj | 17 ++ Frontend-React/package-lock.json | 49 ++++++ Frontend-React/package.json | 1 + Frontend-React/src/App.js | 151 ++++-------------- Frontend-React/src/App.test.js | 41 ++++- Frontend-React/src/components/addTodoItem.js | 68 ++++++++ Frontend-React/src/components/footer.js | 14 ++ Frontend-React/src/components/header.js | 37 +++++ Frontend-React/src/components/todoItems.js | 47 ++++++ Frontend-React/src/index.js | 2 + Frontend-React/src/reportWebVitals.js | 1 + Frontend-React/src/setupTests.js | 1 + 29 files changed, 717 insertions(+), 208 deletions(-) create mode 100644 Backend/TodoList.Api/Models/Models.csproj create mode 100644 Backend/TodoList.Api/Models/TodoItem.cs create mode 100644 Backend/TodoList.Api/Repository/ITodoRepository.cs create mode 100644 Backend/TodoList.Api/Repository/Repository.csproj create mode 100644 Backend/TodoList.Api/TodoList.Models/TodoItem.cs create mode 100644 Backend/TodoList.Api/TodoList.Models/TodoList.Models.csproj create mode 100644 Backend/TodoList.Api/TodoList.RepositoryService/Interface/ITodoRepository.cs create mode 100644 Backend/TodoList.Api/TodoList.RepositoryService/Service/TodoRepository.cs rename Backend/TodoList.Api/{TodoList.Api => TodoList.RepositoryService}/TodoContext.cs (59%) create mode 100644 Backend/TodoList.Api/TodoList.RepositoryService/TodoList.RepositoryService.csproj create mode 100644 Frontend-React/src/components/addTodoItem.js create mode 100644 Frontend-React/src/components/footer.js create mode 100644 Frontend-React/src/components/header.js create mode 100644 Frontend-React/src/components/todoItems.js diff --git a/Backend/TodoList.Api/Models/Models.csproj b/Backend/TodoList.Api/Models/Models.csproj new file mode 100644 index 00000000..132c02c5 --- /dev/null +++ b/Backend/TodoList.Api/Models/Models.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Backend/TodoList.Api/Models/TodoItem.cs b/Backend/TodoList.Api/Models/TodoItem.cs new file mode 100644 index 00000000..1007e23c --- /dev/null +++ b/Backend/TodoList.Api/Models/TodoItem.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Models +{ + public class TodoItem + { + public Guid Id { get; set; } + public string Description { get; set; } + public bool IsCompleted { get; set; } + } +} diff --git a/Backend/TodoList.Api/Repository/ITodoRepository.cs b/Backend/TodoList.Api/Repository/ITodoRepository.cs new file mode 100644 index 00000000..88afec0a --- /dev/null +++ b/Backend/TodoList.Api/Repository/ITodoRepository.cs @@ -0,0 +1,19 @@ +using Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Repository +{ + public interface ITodoRepository + { + Task> GetIncompleteItemsAsync(); + Task GetItemByIdAsync(Guid id); + Task AddItemAsync(TodoItem item); + Task UpdateItemAsync(TodoItem item); + Task ItemExistsAsync(Guid id); + Task DescriptionExistsAsync(string description); + }s +} diff --git a/Backend/TodoList.Api/Repository/Repository.csproj b/Backend/TodoList.Api/Repository/Repository.csproj new file mode 100644 index 00000000..132c02c5 --- /dev/null +++ b/Backend/TodoList.Api/Repository/Repository.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Backend/TodoList.Api/TodoList.Api.UnitTests/DummyTestShould.cs b/Backend/TodoList.Api/TodoList.Api.UnitTests/DummyTestShould.cs index 63656951..40874071 100644 --- a/Backend/TodoList.Api/TodoList.Api.UnitTests/DummyTestShould.cs +++ b/Backend/TodoList.Api/TodoList.Api.UnitTests/DummyTestShould.cs @@ -1,12 +1,66 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoList.Api.Controllers; +using TodoList.Models; +using TodoList.RepositoryService.Interface; using Xunit; namespace TodoList.Api.UnitTests { public class DummyTestShould { + private readonly Mock _mockRepo; + private readonly Mock> _mockLogger; + private readonly TodoItemsController _controller; + + public DummyTestShould() + { + _mockRepo = new Mock(); + _mockLogger = new Mock>(); + _controller = new TodoItemsController(_mockRepo.Object, _mockLogger.Object); + } + [Fact] - public void Test_Should_TestSomething() + public async Task GetTodoItems_ReturnsOkResult_WithAListOfTodoItems() { + // Arrange + _mockRepo.Setup(repo => repo.GetIncompleteItemsAsync()).ReturnsAsync(new List { new TodoItem() }); + + // Act + var result = await _controller.GetTodoItems(); + + // Assert + var okResult = Assert.IsType(result); + var items = Assert.IsType>(okResult.Value); + Assert.Single(items); + } + + [Fact] + public async Task GetTodoItem_ReturnsNotFound_WhenItemDoesNotExist() + { + // Arrange + _mockRepo.Setup(repo => repo.GetItemByIdAsync(It.IsAny())).ReturnsAsync((TodoItem)null); + + // Act + var result = await _controller.GetTodoItem(Guid.NewGuid()); + + // Assert + Assert.IsType(result); + } + + [Fact] + public async Task PostTodoItem_ReturnsBadRequest_WhenDescriptionIsEmpty() + { + // Act + var result = await _controller.PostTodoItem(new TodoItem { Description = "" }); + + // Assert + var badRequestResult = Assert.IsType(result); + Assert.Equal("Description is required", badRequestResult.Value); } } } diff --git a/Backend/TodoList.Api/TodoList.Api.UnitTests/TodoList.Api.UnitTests.csproj b/Backend/TodoList.Api/TodoList.Api.UnitTests/TodoList.Api.UnitTests.csproj index 5956ac35..4cb59016 100644 --- a/Backend/TodoList.Api/TodoList.Api.UnitTests/TodoList.Api.UnitTests.csproj +++ b/Backend/TodoList.Api/TodoList.Api.UnitTests/TodoList.Api.UnitTests.csproj @@ -8,6 +8,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Backend/TodoList.Api/TodoList.Api.sln b/Backend/TodoList.Api/TodoList.Api.sln index de8d1a58..18891c73 100644 --- a/Backend/TodoList.Api/TodoList.Api.sln +++ b/Backend/TodoList.Api/TodoList.Api.sln @@ -1,11 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31613.86 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33829.357 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoList.Api", "TodoList.Api\TodoList.Api.csproj", "{065CE954-2618-4A24-A613-9C336C2154C6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoList.Api.UnitTests", "TodoList.Api.UnitTests\TodoList.Api.UnitTests.csproj", "{2BBBFEED-85B6-4361-85B2-F9E08C86C55B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoList.Api.UnitTests", "TodoList.Api.UnitTests\TodoList.Api.UnitTests.csproj", "{2BBBFEED-85B6-4361-85B2-F9E08C86C55B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoList.RepositoryService", "TodoList.RepositoryService\TodoList.RepositoryService.csproj", "{7EA324A5-B570-4A8A-8D09-1E752C66DFCA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoList.Models", "TodoList.Models\TodoList.Models.csproj", "{8058D951-13DA-4736-90FF-BA3025F390AD}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,6 +25,14 @@ Global {2BBBFEED-85B6-4361-85B2-F9E08C86C55B}.Debug|Any CPU.Build.0 = Debug|Any CPU {2BBBFEED-85B6-4361-85B2-F9E08C86C55B}.Release|Any CPU.ActiveCfg = Release|Any CPU {2BBBFEED-85B6-4361-85B2-F9E08C86C55B}.Release|Any CPU.Build.0 = Release|Any CPU + {7EA324A5-B570-4A8A-8D09-1E752C66DFCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EA324A5-B570-4A8A-8D09-1E752C66DFCA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EA324A5-B570-4A8A-8D09-1E752C66DFCA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EA324A5-B570-4A8A-8D09-1E752C66DFCA}.Release|Any CPU.Build.0 = Release|Any CPU + {8058D951-13DA-4736-90FF-BA3025F390AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8058D951-13DA-4736-90FF-BA3025F390AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8058D951-13DA-4736-90FF-BA3025F390AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8058D951-13DA-4736-90FF-BA3025F390AD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Backend/TodoList.Api/TodoList.Api/Controllers/TodoItemsController.cs b/Backend/TodoList.Api/TodoList.Api/Controllers/TodoItemsController.cs index c42f2f44..352da6e3 100644 --- a/Backend/TodoList.Api/TodoList.Api/Controllers/TodoItemsController.cs +++ b/Backend/TodoList.Api/TodoList.Api/Controllers/TodoItemsController.cs @@ -4,6 +4,8 @@ using System; using System.Linq; using System.Threading.Tasks; +using TodoList.Models; +using TodoList.RepositoryService.Interface; namespace TodoList.Api.Controllers { @@ -11,95 +13,136 @@ namespace TodoList.Api.Controllers [ApiController] public class TodoItemsController : ControllerBase { - private readonly TodoContext _context; + private readonly ITodoRepository _todoRepository; private readonly ILogger _logger; - public TodoItemsController(TodoContext context, ILogger logger) + public TodoItemsController(ITodoRepository todoRepository, ILogger logger) { - _context = context; + _todoRepository = todoRepository; _logger = logger; } - // GET: api/TodoItems + + /// + /// Get Todo Items + /// + /// Todo Items [HttpGet] public async Task GetTodoItems() { - var results = await _context.TodoItems.Where(x => !x.IsCompleted).ToListAsync(); - return Ok(results); + try + { + var results = await _todoRepository.GetIncompleteItemsAsync(); + return Ok(results); + } + catch (Exception ex) + { + _logger.LogError(ex, "TodoItemsController => GetTodoItems"); + throw; + } + } - // GET: api/TodoItems/... + /// + /// Get Todo Item + /// + /// + /// Todo Item [HttpGet("{id}")] public async Task GetTodoItem(Guid id) { - var result = await _context.TodoItems.FindAsync(id); - - if (result == null) + try + { + var result = await _todoRepository.GetItemByIdAsync(id); + if (result == null) + { + return NotFound(); + } + return Ok(result); + } + catch (Exception ex) { - return NotFound(); + _logger.LogError(ex, "TodoItemsController => GetTodoItem"); + throw; } - return Ok(result); } - // PUT: api/TodoItems/... + /// + /// Put Todo Item + /// + /// + /// + /// Error or Success Message [HttpPut("{id}")] public async Task PutTodoItem(Guid id, TodoItem todoItem) { - if (id != todoItem.Id) + try { - return BadRequest(); - } - _context.Entry(todoItem).State = EntityState.Modified; + if (id != todoItem.Id) + { + return BadRequest(); + } - try - { - await _context.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException) - { - if (!TodoItemIdExists(id)) + try { - return NotFound(); + await _todoRepository.UpdateItemAsync(todoItem); } - else + catch (DbUpdateConcurrencyException) { - throw; + if (!await _todoRepository.ItemExistsAsync(id)) + { + return NotFound(); + } + else + { + throw; + } } + + return Ok(new { message = "Item has been updated successfully" }); + } + catch (Exception ex) + { + + _logger.LogError(ex, "TodoItemsController => PutTodoItem"); + throw; } + } - return NoContent(); - } - // POST: api/TodoItems + /// + /// Post Todo Item + /// + /// + /// Error or Success Message [HttpPost] public async Task PostTodoItem(TodoItem todoItem) { - if (string.IsNullOrEmpty(todoItem?.Description)) - { - return BadRequest("Description is required"); - } - else if (TodoItemDescriptionExists(todoItem.Description)) + try { - return BadRequest("Description already exists"); - } - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); - - return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem); - } + if (string.IsNullOrEmpty(todoItem?.Description)) + { + return BadRequest("Description is required"); + } + else if (await _todoRepository.DescriptionExistsAsync(todoItem.Description)) + { + return BadRequest("Description already exists"); + } - private bool TodoItemIdExists(Guid id) - { - return _context.TodoItems.Any(x => x.Id == id); - } + await _todoRepository.AddItemAsync(todoItem); + return Ok(new { message = "Item has been added successfully" }); - private bool TodoItemDescriptionExists(string description) - { - return _context.TodoItems - .Any(x => x.Description.ToLowerInvariant() == description.ToLowerInvariant() && !x.IsCompleted); + + } + catch (Exception ex) + { + + _logger.LogError(ex, "TodoItemsController => PostTodoItem"); + throw; + } } } } diff --git a/Backend/TodoList.Api/TodoList.Api/Properties/launchSettings.json b/Backend/TodoList.Api/TodoList.Api/Properties/launchSettings.json index 43840e65..be99e24e 100644 --- a/Backend/TodoList.Api/TodoList.Api/Properties/launchSettings.json +++ b/Backend/TodoList.Api/TodoList.Api/Properties/launchSettings.json @@ -12,8 +12,8 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - //"launchUrl": "swagger", - "launchUrl": "api/todoitems", + "launchUrl": "swagger", + // "launchUrl": "api/todoitems", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -22,8 +22,8 @@ "commandName": "Project", "dotnetRunMessages": "true", "launchBrowser": true, - //"launchUrl": "swagger", - "launchUrl": "api/todoitems", + "launchUrl": "swagger", + //"launchUrl": "api/todoitems", "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/Backend/TodoList.Api/TodoList.Api/Startup.cs b/Backend/TodoList.Api/TodoList.Api/Startup.cs index 1db21ceb..6265bb56 100644 --- a/Backend/TodoList.Api/TodoList.Api/Startup.cs +++ b/Backend/TodoList.Api/TodoList.Api/Startup.cs @@ -5,6 +5,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; +using System; +using TodoList.RepositoryService; +using TodoList.RepositoryService.Interface; +using TodoList.RepositoryService.Service; namespace TodoList.Api { @@ -17,18 +21,17 @@ public Startup(IConfiguration configuration) public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddCors(options => { options.AddPolicy("AllowAllHeaders", - builder => - { - builder.AllowAnyOrigin() - .AllowAnyHeader() - .AllowAnyMethod(); - }); + builder => + { + builder.AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod(); + }); }); services.AddControllers(); @@ -36,11 +39,20 @@ public void ConfigureServices(IServiceCollection services) { c.SwaggerDoc("v1", new OpenApiInfo { Title = "TodoList.Api", Version = "v1" }); }); + + services.AddDbContext(options => + options.UseSqlServer("Server=DESKTOP-F295Q9T\\SQLEXPRESS;Database=TodoListDB;Integrated Security=True;Encrypt=True;TrustServerCertificate=True;", + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.EnableRetryOnFailure( + maxRetryCount: 5, + maxRetryDelay: TimeSpan.FromSeconds(30), + errorNumbersToAdd: null); + })); - services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoItemsDB")); + services.AddScoped(); } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) @@ -51,13 +63,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) } app.UseHttpsRedirection(); - app.UseRouting(); - app.UseCors("AllowAllHeaders"); - app.UseAuthorization(); - app.UseEndpoints(endpoints => { endpoints.MapControllers(); diff --git a/Backend/TodoList.Api/TodoList.Api/TodoList.Api.csproj b/Backend/TodoList.Api/TodoList.Api/TodoList.Api.csproj index d206ff56..bffb9fd1 100644 --- a/Backend/TodoList.Api/TodoList.Api/TodoList.Api.csproj +++ b/Backend/TodoList.Api/TodoList.Api/TodoList.Api.csproj @@ -11,6 +11,10 @@ + + + + @@ -20,6 +24,11 @@ + + + + + diff --git a/Backend/TodoList.Api/TodoList.Api/appsettings.json b/Backend/TodoList.Api/TodoList.Api/appsettings.json index d9d9a9bf..6ae78750 100644 --- a/Backend/TodoList.Api/TodoList.Api/appsettings.json +++ b/Backend/TodoList.Api/TodoList.Api/appsettings.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "DefaultConnection": "Server=DESKTOP-F295Q9T\\SQLEXPRESS;Database=TodoListDB;Encrypt=True;TrustServerCertificate=True;" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/Backend/TodoList.Api/TodoList.Models/TodoItem.cs b/Backend/TodoList.Api/TodoList.Models/TodoItem.cs new file mode 100644 index 00000000..efadf1db --- /dev/null +++ b/Backend/TodoList.Api/TodoList.Models/TodoItem.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TodoList.Models +{ + public class TodoItem + { + public Guid Id { get; set; } + public string Description { get; set; } + public bool IsCompleted { get; set; } + } +} diff --git a/Backend/TodoList.Api/TodoList.Models/TodoList.Models.csproj b/Backend/TodoList.Api/TodoList.Models/TodoList.Models.csproj new file mode 100644 index 00000000..132c02c5 --- /dev/null +++ b/Backend/TodoList.Api/TodoList.Models/TodoList.Models.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Backend/TodoList.Api/TodoList.RepositoryService/Interface/ITodoRepository.cs b/Backend/TodoList.Api/TodoList.RepositoryService/Interface/ITodoRepository.cs new file mode 100644 index 00000000..33698340 --- /dev/null +++ b/Backend/TodoList.Api/TodoList.RepositoryService/Interface/ITodoRepository.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TodoList.Models; + +namespace TodoList.RepositoryService.Interface +{ + public interface ITodoRepository + { + /// + /// Get Incomplete Items Async + /// + /// TodoItems + Task> GetIncompleteItemsAsync(); + + /// + /// Get Item By Id Async + /// + /// + /// TodoItem + Task GetItemByIdAsync(Guid id); + + /// + /// Add Item Async + /// + /// + /// void + Task AddItemAsync(TodoItem item); + + /// + /// Update Item Async + /// + /// + /// void + Task UpdateItemAsync(TodoItem item); + + /// + /// Description Exists Async + /// + /// + /// trur or false + Task ItemExistsAsync(Guid id); + + /// + /// Description Exists Async + /// + /// + /// trur or false + Task DescriptionExistsAsync(string description); + } +} diff --git a/Backend/TodoList.Api/TodoList.RepositoryService/Service/TodoRepository.cs b/Backend/TodoList.Api/TodoList.RepositoryService/Service/TodoRepository.cs new file mode 100644 index 00000000..e9a6753a --- /dev/null +++ b/Backend/TodoList.Api/TodoList.RepositoryService/Service/TodoRepository.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TodoList.Models; +using TodoList.RepositoryService.Interface; + +namespace TodoList.RepositoryService.Service +{ + public class TodoRepository : ITodoRepository + { + private readonly TodoContext _context; + + public TodoRepository(TodoContext context) + { + _context = context; + } + + /// + /// Get Incomplete Items Async + /// + /// TodoItems + public async Task> GetIncompleteItemsAsync() + { + return await _context.TodoItems.ToListAsync(); + } + + /// + /// Get Item By Id Async + /// + /// + /// TodoItem + public async Task GetItemByIdAsync(Guid id) + { + return await _context.TodoItems.FindAsync(id); + } + + /// + /// Add Item Async + /// + /// + /// void + public async Task AddItemAsync(TodoItem item) + { + _context.TodoItems.Add(item); + await _context.SaveChangesAsync(); + } + + /// + /// Update Item Async + /// + /// + /// void + public async Task UpdateItemAsync(TodoItem item) + { + _context.Entry(item).State = EntityState.Modified; + await _context.SaveChangesAsync(); + } + + /// + /// Item Exists Async + /// + /// + /// trur or false + public async Task ItemExistsAsync(Guid id) + { + return await _context.TodoItems.AnyAsync(x => x.Id == id); + } + + /// + /// Description Exists Async + /// + /// + /// trur or false + public async Task DescriptionExistsAsync(string description) + { + return await _context.TodoItems + .AnyAsync(x => string.IsNullOrEmpty(x.Description) && x.Description.ToLowerInvariant() == description.ToLowerInvariant() && !x.IsCompleted); + + } + } +} diff --git a/Backend/TodoList.Api/TodoList.Api/TodoContext.cs b/Backend/TodoList.Api/TodoList.RepositoryService/TodoContext.cs similarity index 59% rename from Backend/TodoList.Api/TodoList.Api/TodoContext.cs rename to Backend/TodoList.Api/TodoList.RepositoryService/TodoContext.cs index 068513bf..cdebd120 100644 --- a/Backend/TodoList.Api/TodoList.Api/TodoContext.cs +++ b/Backend/TodoList.Api/TodoList.RepositoryService/TodoContext.cs @@ -1,14 +1,11 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using TodoList.Models; -namespace TodoList.Api +namespace TodoList.RepositoryService { public class TodoContext : DbContext { - public TodoContext(DbContextOptions options) - : base(options) - { - } - + public TodoContext(DbContextOptions options) : base(options) { } public DbSet TodoItems { get; set; } } } diff --git a/Backend/TodoList.Api/TodoList.RepositoryService/TodoList.RepositoryService.csproj b/Backend/TodoList.Api/TodoList.RepositoryService/TodoList.RepositoryService.csproj new file mode 100644 index 00000000..7abc5aac --- /dev/null +++ b/Backend/TodoList.Api/TodoList.RepositoryService/TodoList.RepositoryService.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + diff --git a/Frontend-React/package-lock.json b/Frontend-React/package-lock.json index 1ceea5be..6bb61bf2 100644 --- a/Frontend-React/package-lock.json +++ b/Frontend-React/package-lock.json @@ -12,6 +12,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", "axios": "^1.2.2", + "axios-mock-adapter": "^1.22.0", "bootstrap": "^5.2.3", "cross-env": "^7.0.3", "node-sass": "^8.0.0", @@ -5033,6 +5034,18 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios-mock-adapter": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz", + "integrity": "sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, "node_modules/axobject-query": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", @@ -9541,6 +9554,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -22297,6 +22332,15 @@ "proxy-from-env": "^1.1.0" } }, + "axios-mock-adapter": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz", + "integrity": "sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw==", + "requires": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + } + }, "axobject-query": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", @@ -25624,6 +25668,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" + }, "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", diff --git a/Frontend-React/package.json b/Frontend-React/package.json index 017ec5a0..97a6ba0d 100644 --- a/Frontend-React/package.json +++ b/Frontend-React/package.json @@ -8,6 +8,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", "axios": "^1.2.2", + "axios-mock-adapter": "^1.22.0", "bootstrap": "^5.2.3", "cross-env": "^7.0.3", "node-sass": "^8.0.0", diff --git a/Frontend-React/src/App.js b/Frontend-React/src/App.js index 6d034540..5f14a97a 100644 --- a/Frontend-React/src/App.js +++ b/Frontend-React/src/App.js @@ -1,163 +1,70 @@ import './App.css' -import { Image, Alert, Button, Container, Row, Col, Form, Table, Stack } from 'react-bootstrap' +import { Alert, Container, Row, Col } from 'react-bootstrap' import React, { useState, useEffect } from 'react' - -const axios = require('axios') +import axios from 'axios' +import Footer from './components/footer' +import Header from './components/header' +import AddTodoItem from './components/addTodoItem' +import TodoItems from './components/todoItems' const App = () => { - const [description, setDescription] = useState('') + const [items, setItems] = useState([]) + const [error, setError] = useState(null); useEffect(() => { - // todo + getItems(); }, []) - const renderAddTodoItemContent = () => { - return ( - -

Add Item

- - - Description - - - - - - - - - - - -
- ) - } - const renderTodoItemsContent = () => { - return ( - <> -

- Showing {items.length} Item(s){' '} - -

- - - - - - - - - - - {items.map((item) => ( - - - - - - ))} - -
IdDescriptionAction
{item.id}{item.description} - -
- - ) - } - - const handleDescriptionChange = (event) => { - // todo + const handleErrorMessage = (error) => { + setError(error) + setTimeout(() => { + setError(null) + }, 3000) } + //Get Items async function getItems() { try { - alert('todo') + const response = await axios.get('https://localhost:44397/api/TodoItems'); + setItems(response?.data || []); } catch (error) { - console.error(error) + handleErrorMessage(error.response?.data?.message || 'Failed to fetch Todo items. Please try again.') } } - async function handleAdd() { - try { - alert('todo') - } catch (error) { - console.error(error) - } - } - - function handleClear() { - setDescription('') - } + //Get Item async function handleMarkAsComplete(item) { try { - alert('todo') + await axios.put(`https://localhost:44397/api/TodoItems/${item.id}`, { id: item.id, description: item.description, isCompleted: true }); + getItems(); } catch (error) { - console.error(error) + handleErrorMessage(error.response?.data?.message || 'Failed to mark Todo item as completed. Please try again.') } } return (
+
- - - + {error && {error}} - - Todo List App - Welcome to the ClearPoint frontend technical test. We like to keep things simple, yet clean so your - task(s) are as follows: -
-
-
    -
  1. Add the ability to add (POST) a Todo Item by calling the backend API
  2. -
  3. - Display (GET) all the current Todo Items in the below grid and display them in any order you wish -
  4. -
  5. - Bonus points for completing the 'Mark as completed' button code for allowing users to update and mark - a specific Todo Item as completed and for displaying any relevant validation errors/ messages from the - API in the UI -
  6. -
  7. Feel free to add unit tests and refactor the component(s) as best you see fit
  8. -
-
+
- - {renderAddTodoItemContent()} -
- {renderTodoItemsContent()} + + + - +
) } diff --git a/Frontend-React/src/App.test.js b/Frontend-React/src/App.test.js index 1e2077f1..dca209ae 100644 --- a/Frontend-React/src/App.test.js +++ b/Frontend-React/src/App.test.js @@ -1,8 +1,33 @@ -import { render, screen } from '@testing-library/react' -import App from './App' - -test('renders the footer text', () => { - render() - const footerElement = screen.getByText(/clearpoint.digital/i) - expect(footerElement).toBeInTheDocument() -}) +import React from 'react'; +import { render, screen, waitFor, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import App from './App'; + + +const mock = new MockAdapter(axios); + +describe('App Component', () => { + beforeEach(() => { + mock.reset(); + }); + + test('fetches and displays todo items', async () => { + const items = [{ id: 1, description: 'Test Item', isCompleted: false }]; + mock.onGet('https://localhost:44397/api/TodoItems').reply(200, items); + + render(); + + await waitFor(() => expect(screen.getByText(/Test Item/i)).toBeInTheDocument()); + }); + + test('displays an error message on fetch failure', async () => { + mock.onGet('https://localhost:44397/api/TodoItems').reply(500); + + render(); + + await waitFor(() => expect(screen.getByText(/Failed to fetch Todo items. Please try again./i)).toBeInTheDocument()); + }); + +}); \ No newline at end of file diff --git a/Frontend-React/src/components/addTodoItem.js b/Frontend-React/src/components/addTodoItem.js new file mode 100644 index 00000000..88e78622 --- /dev/null +++ b/Frontend-React/src/components/addTodoItem.js @@ -0,0 +1,68 @@ +import { Container, Row, Col, Form, Button, Stack } from 'react-bootstrap'; +import React, { useState } from 'react'; +import axios from 'axios' + + +const AddTodoItem = ({ getItems, handleErrorMessage }) => { + + const [description, setDescription] = useState('') + + const handleDescriptionChange = (event) => { + setDescription(event.target.value) + } + + function handleClear() { + setDescription('') + } + + function generateUUID() { + // The template string where 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' will be replaced + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + async function handleAdd() { + try { + const response = await axios.post('https://localhost:44397/api/TodoItems', { id: generateUUID(), description: description, isCompleted: false }); + if (response.status === 200) { + setDescription(''); + getItems() + } + } catch (error) { + handleErrorMessage(error.response?.data?.message || 'Failed to add Todo item. Please try again.') + } + } + + return ( + +

Add Item

+ + + Description + + + + + + + + + + + +
+ ); +}; + +export default AddTodoItem; diff --git a/Frontend-React/src/components/footer.js b/Frontend-React/src/components/footer.js new file mode 100644 index 00000000..fe09e405 --- /dev/null +++ b/Frontend-React/src/components/footer.js @@ -0,0 +1,14 @@ +const Footer = () => { + + return ( + + ) +} +export default Footer diff --git a/Frontend-React/src/components/header.js b/Frontend-React/src/components/header.js new file mode 100644 index 00000000..c0017f71 --- /dev/null +++ b/Frontend-React/src/components/header.js @@ -0,0 +1,37 @@ +import { Image, Alert, Row, Col} from 'react-bootstrap' + +const Header = () => { + return ( + <> + + + + + + + + + Todo List App + Welcome to the ClearPoint frontend technical test. We like to keep things simple, yet clean so your + task(s) are as follows: +
+
+
    +
  1. Add the ability to add (POST) a Todo Item by calling the backend API
  2. +
  3. + Display (GET) all the current Todo Items in the below grid and display them in any order you wish +
  4. +
  5. + Bonus points for completing the 'Mark as completed' button code for allowing users to update and mark + a specific Todo Item as completed and for displaying any relevant validation errors/ messages from the + API in the UI +
  6. +
  7. Feel free to add unit tests and refactor the component(s) as best you see fit
  8. +
+
+ +
+ + ) +} +export default Header diff --git a/Frontend-React/src/components/todoItems.js b/Frontend-React/src/components/todoItems.js new file mode 100644 index 00000000..2b03878b --- /dev/null +++ b/Frontend-React/src/components/todoItems.js @@ -0,0 +1,47 @@ +import { Button, Table } from 'react-bootstrap'; +import React from 'react'; + + +const TodoItems = ({ items, getItems, handleMarkAsComplete }) => { + return ( + <> +

+ Showing {items.length} Item(s){' '} + +

+ + + + + + + + + + + {items.map((item) => ( + + + + + + ))} + +
IdDescriptionAction
{item.id}{item.description} + {item.isCompleted ? + : + } + + +
+ + ); +}; + +export default TodoItems; diff --git a/Frontend-React/src/index.js b/Frontend-React/src/index.js index 7e845312..02714b48 100644 --- a/Frontend-React/src/index.js +++ b/Frontend-React/src/index.js @@ -5,6 +5,8 @@ import App from './App' import reportWebVitals from './reportWebVitals' import 'bootstrap/dist/css/bootstrap.min.css' + + const root = createRoot(document.getElementById('root')) root.render( diff --git a/Frontend-React/src/reportWebVitals.js b/Frontend-React/src/reportWebVitals.js index dc6ff078..cdb0303d 100644 --- a/Frontend-React/src/reportWebVitals.js +++ b/Frontend-React/src/reportWebVitals.js @@ -11,3 +11,4 @@ const reportWebVitals = (onPerfEntry) => { } export default reportWebVitals + diff --git a/Frontend-React/src/setupTests.js b/Frontend-React/src/setupTests.js index 8f2609b7..b11125c0 100644 --- a/Frontend-React/src/setupTests.js +++ b/Frontend-React/src/setupTests.js @@ -3,3 +3,4 @@ // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; +