diff --git a/.gitignore b/.gitignore index 34ca015..e2a1839 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,16 @@ -bin/ -obj/ .vs/ .vscode/ -Migrations/ -app.db* .DS_Store -app.db +bin/ +obj/ +app.db* +Data/Interfaces +Data/UnitOfWork.cs +Data/*Repository.cs +Migrations/ +Properties/ +Tests/TicketManager.Tests/UnitTests/ControllersTests/ControllerTests.cs +Tests/TicketManager.Tests/UnitTests/ControllersTests/SeedDb.cs # client client/src/pages/TestPage.tsx diff --git a/Controllers/AppUsersController.cs b/Controllers/AppUsersController.cs index d3ed49a..98262ef 100644 --- a/Controllers/AppUsersController.cs +++ b/Controllers/AppUsersController.cs @@ -1,38 +1,78 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using TicketManager.Data; using TicketManager.Models; -using Microsoft.AspNetCore.Authorization; +using TicketManager.DTO; + namespace TicketManager.Controllers { // [Authorize] + [Produces("application/json")] [Route("api/v1/users")] [ApiController] public class UsersController : ControllerBase { - private readonly IAppUserRepository _users; + private readonly AppDbContext _context; - public UsersController(IAppUserRepository users) + public UsersController(AppDbContext context) { - _users = users; + _context = context; } - // GET: api/Users + /// + /// Returns all Users stored in the database. + /// + /// + /// Sample request: + /// + /// GET: api/v1/Users + /// + /// + /// Returns a list of users [HttpGet] - public async Task> GetUsers() + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetUsers() { - return await _users.List(); + return await _context.AppUsers + .Include(u => u.Assignments) + .ThenInclude(a => a.Project) + .Include(u => u.Activities) + .AsNoTracking() + .Select(u => new AppUserDTO(u)) + .ToListAsync(); } - // GET: api/Users/5 + /// + /// Locate a specific User stored in the database by its Id + /// + /// + /// Sample request: + /// + /// GET: api/v1/Users/2 + /// + /// + /// Returns a User object + /// If the required User is null [HttpGet("{id}")] - public async Task> GetUser(Guid id) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetUser(Guid id) { - var user = await _users.GetUser(id); + var user = await _context.AppUsers + .Include(u => u.Assignments) + .ThenInclude(a => a.Project) + .Include(u => u.Activities) + .AsNoTracking() + .Select(u => new AppUserDTO(u)) + .FirstOrDefaultAsync(u => u.Id == id); + if (user == null) { return NotFound(); @@ -40,23 +80,44 @@ namespace TicketManager.Controllers return user; } - // PUT: api/Users/5 - // To protect from overposting attacks, please enable the specific properties you want to bind to, for - // more details see https://aka.ms/RazorPagesCRUD. + /// + /// Updates the specific project with Id. + /// + /// + /// Sample request: + /// + /// PUT: api/v1/Projects/3 + /// { + /// "id": "357727fd-5262-4522-b8a3-38271d43de84", + /// "firstName": "Thomas", + /// "lastName": "Price", + /// "presentation": "New Team?!", + /// "email": "tp@mail.com", + /// "phone": "0198237645" + /// } + /// + /// + /// Request was succesful but no content is changed + /// If the required project is null [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task PutUser(Guid id, AppUser user) { if (id != user.Id) { return BadRequest(); } + + _context.Entry(user).State = EntityState.Modified; + try { - await _users.Update(user); + await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { - if (!_users.Exists(id)) + if (!UserExists(id)) { return NotFound(); } @@ -68,49 +129,101 @@ namespace TicketManager.Controllers return NoContent(); } - // POST: api/Users - // To protect from overposting attacks, please enable the specific properties you want to bind to, for - // more details see https://aka.ms/RazorPagesCRUD. + /// + /// Creates a project. + /// + /// + /// Sample request: + /// + /// POST: api/v1/Projects/ + /// { + /// "firstName": "Thomas", + /// "lastName": "Price", + /// "presentation": "New Team?!", + /// "email": "tp@mail.com", + /// "phone": "0198237645" + /// } + /// + /// + /// Returns the created project [HttpPost] - public async Task> PostUser(AppUser user) + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> PostUser(AppUser user) { - await _users.Add(user); - return CreatedAtAction("GetUser", new { id = user.Id }, user); + if (!ModelState.IsValid) + { + return BadRequest(); + } + + _context.AppUsers.Add(user); + await _context.SaveChangesAsync(); + + var dto = new AppUserDTO(user); + + return CreatedAtAction("GetUser", new { id = user.Id }, dto); } - // DELETE: api/Users/5 + /// + /// Deletes the project identified by its Id + /// + /// + /// Sample request: + /// + /// DELETE: api/v1/Projects/5 + /// + /// + /// Returns the deleted project + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] [HttpDelete("{id}")] - public async Task> DeleteUser(Guid id) + public async Task> DeleteUser(Guid id) { - var user = await _users.GetUser(id); + var user = await _context.AppUsers.FindAsync(id); if (user == null) { return NotFound(); } - await _users.Delete(user); - return user; + _context.AppUsers.Remove(user); + await _context.SaveChangesAsync(); + var dto = new AppUserDTO(user); + return Ok(user); } [HttpGet("{id}/projects")] - public async Task>> GetAppUserProjects(Guid id) + public async Task>> GetAppUserProjects(Guid id) { - AppUser user = await _users.GetUser(id); + var user = await _context.AppUsers + .Include(u => u.Assignments) + .ThenInclude(a => a.Project) + .AsNoTracking() + .FirstOrDefaultAsync(u => u.Id == id); if (user == null) { return BadRequest(); } - return user.GetProjects(); + return user.GetProjects().Select(p => new ProjectDTO(p)).ToList(); } [HttpGet("{id}/tickets/")] - public async Task>> GetAppUserTickets(Guid id) + public async Task>> GetAppUserTickets(Guid id) { - AppUser user = await _users.GetUser(id); + var user = await _context.AppUsers + .Include(u => u.Assignments) + .ThenInclude(a => a.Project) + .ThenInclude(p => p.Tickets) + .AsNoTracking() + .FirstOrDefaultAsync(u => u.Id == id); if (user == null) { return BadRequest(); } - return user.GetTickets(); + return user.GetTickets().Select(t => new TicketDTO(t)).ToList(); + } + + private bool UserExists(Guid id) + { + return _context.AppUsers.Any(e => e.Id == id); } } } diff --git a/Controllers/ProjectsController.cs b/Controllers/ProjectsController.cs index d8232d9..0b98104 100644 --- a/Controllers/ProjectsController.cs +++ b/Controllers/ProjectsController.cs @@ -18,10 +18,11 @@ namespace TicketManager.Controllers [ApiController] public class ProjectsController : ControllerBase { - private IProjectRepository _projects; - public ProjectsController(IProjectRepository context) + private readonly AppDbContext _context; + + public ProjectsController(AppDbContext context) { - _projects = context; + _context = context; } /// @@ -36,10 +37,18 @@ namespace TicketManager.Controllers /// Returns a list of projects [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetProjects() + public async Task> GetProjects() { - - return await _projects.List(); + return await _context.Projects + .Include(p => p.Assignments) + .ThenInclude(a => a.User) + .Include(p => p.Tickets) + .Include(p => p.Manager) + .Include(p => p.Files) + .Include(p => p.Activities) + .AsNoTracking() + .Select(p => new ProjectDTO(p)) + .ToListAsync(); } /// @@ -51,19 +60,30 @@ namespace TicketManager.Controllers /// GET: api/v1/Projects/2 /// /// - /// Returns a project object + /// Identifier of the ressource + /// Returns a specific project /// If the required project is null [HttpGet("{id}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetProject(int id) { - Project project = await _projects.Get(id); + var project = await _context.Projects + .Include(p => p.Assignments) + .ThenInclude(a => a.User) + .Include(p => p.Tickets) + .Include(p => p.Manager) + .Include(p => p.Files) + .Include(p => p.Activities) + .AsNoTracking() + .Select(p => new ProjectDTO(p)) + .FirstOrDefaultAsync(p => p.Id == id); + if (project == null) { return NotFound(); } - return new ProjectDTO(project); + return project; } /// @@ -90,15 +110,25 @@ namespace TicketManager.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task PutProject(int id, Project project) { - if (id != project.Id) { return BadRequest(); } + if (id != project.Id) + { + return BadRequest(); + } + _context.Entry(project).State = EntityState.Modified; try { - await _projects.Update(project); + await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { - if (!_projects.Exists(id)) { return NotFound(); } - else { throw; } + if (!ProjectExists(id)) + { + return NotFound(); + } + else + { + throw; + } } return NoContent(); } @@ -123,11 +153,16 @@ namespace TicketManager.Controllers [HttpPost] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> PostProject(Project project) + public async Task> PostProject(Project project) { - if (!ModelState.IsValid) { return BadRequest(); } - await _projects.Add(project); - return CreatedAtAction("GetProject", new { id = project.Id }, project); + if (!ModelState.IsValid) + { + return BadRequest(); + } + _context.Projects.Add(project); + await _context.SaveChangesAsync(); + var dto = new ProjectDTO(project); + return CreatedAtAction("GetProject", new { id = project.Id }, dto); } /// @@ -145,13 +180,15 @@ namespace TicketManager.Controllers [HttpDelete("{id}")] public async Task DeleteProject(int id) { - var project = await _projects.Get(id); + var project = await _context.Projects.FindAsync(id); if (project == null) { return NotFound(); } - await _projects.Delete(project); - return Ok(); + _context.Projects.Remove(project); + await _context.SaveChangesAsync(); + var dto = new ProjectDTO(project); + return Ok(dto); } /// @@ -169,9 +206,20 @@ namespace TicketManager.Controllers [HttpGet("{id}/members")] public async Task>> GetProjectMembers(int id) { - var project = await _projects.Get(id); + Project project = await _context.Projects + .Include(p => p.Assignments) + .ThenInclude(a => a.User) + .Include(p => p.Tickets) + .Include(p => p.Manager) + .Include(p => p.Files) + .Include(p => p.Activities) + .AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == id); + if (project == null) - { return NotFound(); } + { + return NotFound(); + } return project.GetMembers(); } @@ -195,18 +243,24 @@ namespace TicketManager.Controllers /// Not Found [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [HttpPut("{id}/members")] + [HttpPatch("{id}/members")] public async Task> SetProjectMembers(int id, List projectMembers) { - Project project = await _projects.Get(id); + Project project = await _context.Projects + .Include(p => p.Assignments) + .FirstOrDefaultAsync(p => p.Id == id); + if (project == null) { return NotFound(); } + project.SetMembers(projectMembers); + _context.Entry(project).State = EntityState.Modified; try { - await _projects.Update(project); + + await _context.SaveChangesAsync(); } catch (DbUpdateException /* ex */) { @@ -218,86 +272,9 @@ namespace TicketManager.Controllers return NoContent(); } - // // /// - // // /// Assign a user to a project. - // // /// - // // /// - // // /// Sample request: - // // /// - // // /// POST: api/v1/Projects/addmembers - // // /// [{ - // // /// "id": "357727fd-5262-4522-b8a3-38271d43de84", - // // /// "firstName": "Thomas", - // // /// "lastName": "Price", - // // /// "presentation": "New Team?!", - // // /// "email": "tp@mail.com", - // // /// "phone": "0198237645" - // // /// }] - // // /// - // // /// - // // /// Returns the created project - // // [ProducesResponseType(StatusCodes.Status204NoContent)] - // // [ProducesResponseType(StatusCodes.Status404NotFound)] - // // [HttpPut("{id}/addMembers")] - // // public async Task> AddMembersToProject(int id, List usersToAdd) - // // { - // // if (usersToAdd == null) - // // { - // // return BadRequest(); - // // } - // // Project project = await GetProjectByIdAsync(id); - // // project.AddMembers(usersToAdd); - // // try - // // { - // // await _context.SaveChangesAsync(); - // // } - // // catch (DbUpdateException /* ex */) - // // { - // // //Log the error (uncomment ex variable name and write a log.) - // // ModelState.AddModelError("", "Unable to save changes. " + - // // "Try again, and if the problem persists, " + - // // "see your system administrator."); - // // } - // // return NoContent(); - // // } - - // // /// - // // /// Remove a user to a project. - // // /// - // // /// - // // /// Sample request: - // // /// - // // /// PUT: api/v1/Projects/removemembers - // // /// [{ - // // /// "id": "357727fd-5262-4522-b8a3-38271d43de84", - // // /// "firstName": "Thomas", - // // /// "lastName": "Price", - // // /// "presentation": "New Team?!", - // // /// "email": "tp@mail.com", - // // /// "phone": "0198237645" - // // /// }] - // // /// - // // /// - // // /// Returns the created project - // // [ProducesResponseType(StatusCodes.Status204NoContent)] - // // [ProducesResponseType(StatusCodes.Status404NotFound)] - // // [HttpPut("{id}/removeMembers")] - // // public async Task> RemoveMembersFromProject(int id, List usersToRemove) - // // { - // // Project project = await GetProjectByIdAsync(id); - // // project.RemoveMembers(usersToRemove); - // // try - // // { - // // await _context.SaveChangesAsync(); - // // } - // // catch (DbUpdateException /* ex */) - // // { - // // //Log the error (uncomment ex variable name and write a log.) - // // ModelState.AddModelError("", "Unable to save changes. " + - // // "Try again, and if the problem persists, " + - // // "see your system administrator."); - // // } - // // return NoContent(); - // // } + private bool ProjectExists(int id) + { + return _context.Projects.Any(e => e.Id == id); + } } } diff --git a/Controllers/TicketsController.cs b/Controllers/TicketsController.cs index 90844f0..cc94ec6 100644 --- a/Controllers/TicketsController.cs +++ b/Controllers/TicketsController.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using TicketManager.Data; +using TicketManager.DTO; using TicketManager.Models; namespace TicketManager.Controllers @@ -13,25 +15,40 @@ namespace TicketManager.Controllers [ApiController] public class TicketsController : ControllerBase { - private readonly ITicketRepository _tickets; + private readonly AppDbContext _context; - public TicketsController(ITicketRepository tickets) + public TicketsController(AppDbContext context) { - _tickets = tickets; + _context = context; } // GET: api/Tickets [HttpGet] - public async Task> GetTickets() + public async Task> GetTickets() { - return await _tickets.List(); + return await _context.Tickets + .Include(t => t.Project) + .Include(t => t.Files) + .Include(t => t.Activities) + .Include(t => t.Notes) + .AsNoTracking() + .Select(t => new TicketDTO(t)) + .ToListAsync(); } // GET: api/Tickets/5 [HttpGet("{id}")] - public async Task> GetTicket(int id) + public async Task> GetTicket(int id) { - var ticket = await _tickets.Get(id); + var ticket = await _context.Tickets + .Include(t => t.Project) + .Include(t => t.Files) + .Include(t => t.Activities) + .Include(t => t.Notes) + .AsNoTracking() + .Select(t => new TicketDTO(t)) + .FirstOrDefaultAsync(t => t.Id == id); + if (ticket == null) { return NotFound(); @@ -49,13 +66,14 @@ namespace TicketManager.Controllers { return BadRequest(); } + _context.Entry(ticket).State = EntityState.Modified; try { - await _tickets.Update(ticket); + await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { - if (!_tickets.Exists(id)) + if (!TicketExists(id)) { return NotFound(); } @@ -73,36 +91,56 @@ namespace TicketManager.Controllers [HttpPost] public async Task> PostTicket(Ticket ticket) { - await _tickets.Add(ticket); - return CreatedAtAction("GetTicket", new { id = ticket.Id }, ticket); + _context.Tickets.Add(ticket); + await _context.SaveChangesAsync(); + + var dto = new TicketDTO(ticket); + return CreatedAtAction("GetTicket", new { id = ticket.Id }, dto); } // DELETE: api/Tickets/5 [HttpDelete("{id}")] - public async Task> DeleteTicket(int id) + public async Task> DeleteTicket(int id) { - var ticket = await _tickets.Get(id); + var ticket = await _context.Tickets.FindAsync(id); if (ticket == null) { return NotFound(); } - await _tickets.Delete(ticket); - return ticket; + + _context.Tickets.Remove(ticket); + await _context.SaveChangesAsync(); + + var dto = new TicketDTO(ticket); + return Ok(dto); } [HttpGet("{id}/assignees")] - public async Task>> GetTicketAssignees(int id) + public async Task>> GetTicketAssignees(int id) { - Ticket ticket = await _tickets.Get(id); - return ticket.GetAssignees(); + Ticket ticket = await _context.Tickets + .Include(t => t.Project) + .ThenInclude(p => p.Assignments) + .ThenInclude(a => a.User) + .AsNoTracking() + .FirstOrDefaultAsync(t => t.Id == id); + + return ticket.GetAssignees().Select(u => new AppUserDTO(u)).ToList(); } [HttpPut("{id}/closed")] public async Task CloseTicket(int id) { - Ticket ticket = await _tickets.Get(id); + Ticket ticket = await _context.Tickets.FindAsync(id); ticket.Close(); + // _context.Entry(ticket).State = EntityState.Modified; + return await PutTicket(ticket.Id, ticket); } + + private bool TicketExists(int id) + { + return _context.Tickets.Any(e => e.Id == id); + } } } diff --git a/DTOs/AppUserDTO.cs b/DTOs/AppUserDTO.cs new file mode 100644 index 0000000..b2009d3 --- /dev/null +++ b/DTOs/AppUserDTO.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using TicketManager.Models; + +namespace TicketManager.DTO +{ + public class AppUserDTO + { + public AppUserDTO(AppUser user) + { + Id = user.Id; + FirstName = user.FirstName; + LastName = user.LastName; + Presentation = user.Presentation; + Email = user.Email; + Phone = user.Phone; + Created_at = user.Created_at; + Picture = user.Picture; + Activities = user.Activities; + Projects = user.GetProjects(); + Tickets = user.GetTickets(); + } + + public Guid Id { get; set; } + + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string FullName => $"{FirstName} {LastName}"; + + public string Presentation { get; set; } + + [DataType(DataType.EmailAddress)] + public string Email { get; set; } + + [DataType(DataType.PhoneNumber)] + public string Phone { get; set; } + + [DataType(DataType.Date)] + public DateTime Created_at { get; private set; } = DateTime.Now; + + public string Picture { get; set; } + + public List Activities { get; set; } = new List(); + + public List Projects { get; set; } = new List(); + + public List Tickets { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/DataTransfertObjects/ProjectDTO.cs b/DTOs/ProjectDTO.cs similarity index 99% rename from DataTransfertObjects/ProjectDTO.cs rename to DTOs/ProjectDTO.cs index ccfa8a3..ba8d098 100644 --- a/DataTransfertObjects/ProjectDTO.cs +++ b/DTOs/ProjectDTO.cs @@ -36,6 +36,7 @@ namespace TicketManager.DTO public string Status { get; set; } public AppUser Manager { get; set; } + public List Users { get; set; } = new List(); public List Tickets { get; set; } = new List(); diff --git a/DTOs/TicketDTO.cs b/DTOs/TicketDTO.cs new file mode 100644 index 0000000..cdacb99 --- /dev/null +++ b/DTOs/TicketDTO.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using TicketManager.Models; + +namespace TicketManager.DTO +{ + public class TicketDTO + { + public TicketDTO(Ticket ticket) + { + Id = ticket.Id; + Title = ticket.Title; + Description = ticket.Description; + CreatedAt = ticket.CreatedAt; + PlannedEnding = ticket.PlannedEnding; + Status = ticket.Status.ToString(); + Impact = ticket.Impact.ToString(); + Difficulty = ticket.Difficulty.ToString(); + Category = ticket.Category.ToString(); + CreatorId = ticket.CreatorId; + Project = ticket.Project; + Notes = ticket.Notes; + Activities = ticket.Activities; + Files = ticket.Files; + Users = ticket.GetAssignees(); + } + public int Id { get; set; } + + public string Title { get; set; } + + public string Description { get; set; } + + [DataType(DataType.Date)] + public DateTime CreatedAt { get; private set; } + + [DataType(DataType.Date)] + public DateTime PlannedEnding { get; set; } + + public string Status { get; set; } + + public string Impact { get; set; } + + public string Difficulty { get; set; } + + public string Category { get; set; } + + public Guid CreatorId { get; set; } + + public Project Project { get; set; } + + public List Notes { get; set; } = new List(); + + public List Activities { get; set; } = new List(); + + public List Files { get; set; } = new List(); + + public List Users { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/Data/AppUserRepository.cs b/Data/AppUserRepository.cs deleted file mode 100644 index a8b4a62..0000000 --- a/Data/AppUserRepository.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Threading.Tasks; -using TicketManager.Models; -using System.Linq; -using System; -using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; - -namespace TicketManager.Data -{ - public class AppUserRepository : GenericRepository, IAppUserRepository - { - private readonly IQueryable _query; - public AppUserRepository(AppDbContext context) : base(context) - { - _query = _dbSet - .Include(p => p.Assignments) - .ThenInclude(a => a.Project) - .ThenInclude(p => p.Tickets) - .Include(p => p.Activities); - } - - public async Task GetUser(Guid id) - { - return await _query.FirstOrDefaultAsync(p => p.Id == id); - } - - public override async Task> List() - { - return await _query.ToListAsync(); - } - - public bool Exists(Guid id) - { - return _dbSet.Any(e => e.Id == id); - } - } -} \ No newline at end of file diff --git a/Data/GenericRepository.cs b/Data/GenericRepository.cs deleted file mode 100644 index 1a90aa1..0000000 --- a/Data/GenericRepository.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; - -namespace TicketManager.Data -{ - public class GenericRepository : IGenericRepository where T : class - { - protected readonly AppDbContext _context; - protected readonly DbSet _dbSet; - public GenericRepository(AppDbContext context) - { - _context = context; - _dbSet = _context.Set(); - } - - public async Task Add(T entity) - { - _dbSet.Add(entity); - return await _context.SaveChangesAsync(); - } - - public async Task Delete(T entity) - { - if (_context.Entry(entity).State == EntityState.Detached) - { _dbSet.Attach(entity); } - _dbSet.Remove(entity); - return await _context.SaveChangesAsync(); - } - - public async Task> Find(int id, Expression> expr) - { - return await _dbSet.Where(expr).AsNoTracking().ToListAsync(); - } - - public virtual async Task Get(int id) - { - return await _dbSet.FindAsync(id); - } - public virtual async Task> List() - { - return await _dbSet.AsNoTracking().ToListAsync(); - } - - public async Task Update(T entity) - { - _dbSet.Attach(entity); - _context.Entry(entity).State = EntityState.Modified; - return await _context.SaveChangesAsync(); - } - } -} \ No newline at end of file diff --git a/Data/Interfaces/IAppUserRepository.cs b/Data/Interfaces/IAppUserRepository.cs deleted file mode 100644 index ab57db3..0000000 --- a/Data/Interfaces/IAppUserRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Threading.Tasks; -using TicketManager.Models; - -namespace TicketManager.Data -{ - public interface IAppUserRepository : IGenericRepository - { - Task GetUser(Guid id); - bool Exists(Guid id); - } -} \ No newline at end of file diff --git a/Data/Interfaces/IGenericRepository.cs b/Data/Interfaces/IGenericRepository.cs deleted file mode 100644 index 915ef12..0000000 --- a/Data/Interfaces/IGenericRepository.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Linq.Expressions; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace TicketManager.Data -{ - public interface IGenericRepository where T : class - { - Task> List(); - Task Get(int id); - Task> Find(int id, Expression> expr); - - Task Add(T entity); - - Task Update(T entity); - - Task Delete(T entity); - } -} \ No newline at end of file diff --git a/Data/Interfaces/IProjectRepository.cs b/Data/Interfaces/IProjectRepository.cs deleted file mode 100644 index ea8c756..0000000 --- a/Data/Interfaces/IProjectRepository.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using TicketManager.Models; - -namespace TicketManager.Data -{ - public interface IProjectRepository : IGenericRepository - { - bool Exists(int id); - Task> GetMembers(int id); - Task SetMembers(int id, List usersToAdd); - } -} \ No newline at end of file diff --git a/Data/Interfaces/ITicketRepository.cs b/Data/Interfaces/ITicketRepository.cs deleted file mode 100644 index de8af26..0000000 --- a/Data/Interfaces/ITicketRepository.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using TicketManager.Models; - -namespace TicketManager.Data -{ - public interface ITicketRepository : IGenericRepository - { - bool Exists(int id); - } -} \ No newline at end of file diff --git a/Data/Interfaces/IUnitOfWork.cs b/Data/Interfaces/IUnitOfWork.cs deleted file mode 100644 index eab4e21..0000000 --- a/Data/Interfaces/IUnitOfWork.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace TicketManager.Data -{ - public interface IUnitOfWork : IDisposable - { - IProjectRepository Projects { get; } - IAppUserRepository AppUsers { get; } - ITicketRepository Tickets { get; } - Task Complete(); - } -} \ No newline at end of file diff --git a/Data/ProjectRepository.cs b/Data/ProjectRepository.cs deleted file mode 100644 index ec8dede..0000000 --- a/Data/ProjectRepository.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Threading.Tasks; -using TicketManager.Models; -using System.Linq; -using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; - -namespace TicketManager.Data -{ - public class ProjectRepository : GenericRepository, IProjectRepository - { - private readonly IQueryable _query; - public ProjectRepository(AppDbContext context) : base(context) - { - _query = _dbSet - .Include(p => p.Assignments).ThenInclude(a => a.User) - .Include(p => p.Tickets) - .Include(p => p.Manager) - .Include(p => p.Files); - } - - public override async Task Get(int id) - { - return await _query.FirstOrDefaultAsync(p => p.Id == id); - } - - public override async Task> List() - { - return await _query.ToListAsync(); - } - - public bool Exists(int id) - { - return _dbSet.Any(e => e.Id == id); - } - - public async Task> GetMembers(int id) - { - Project project = await Get(id); - return project.GetMembers(); - } - public async Task SetMembers(int id, List usersToAdd) - { - Project project = await Get(id); - project.SetMembers(usersToAdd); - } - } -} \ No newline at end of file diff --git a/Data/TicketRepository.cs b/Data/TicketRepository.cs deleted file mode 100644 index e142c8f..0000000 --- a/Data/TicketRepository.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using System.Linq; -using TicketManager.Models; -using Microsoft.EntityFrameworkCore; - -namespace TicketManager.Data -{ - public class TicketRepository : GenericRepository, ITicketRepository - { - private IQueryable _query; - public TicketRepository(AppDbContext context) : base(context) - { - _query = _dbSet - .Include(p => p.Project) - .ThenInclude(a => a.Assignments) - .ThenInclude(p => p.User) - // .Include(p => p.Edits) - // .Include(p => p.Notes) - // .Include(p => p.Files) - // .Include(p => p.Creator) - ; - } - - public override async Task Get(int id) - { - return await _query.FirstOrDefaultAsync(p => p.Id == id); - } - - public override async Task> List() - { - return await _query.ToListAsync(); - } - - public bool Exists(int id) - { - return _dbSet.Any(e => e.Id == id); - } - } -} \ No newline at end of file diff --git a/Data/UnitOfWork.cs b/Data/UnitOfWork.cs deleted file mode 100644 index 1a0e21a..0000000 --- a/Data/UnitOfWork.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace TicketManager.Data -{ - public class UnitOfWork : IUnitOfWork - { - private readonly AppDbContext _context; - public UnitOfWork(AppDbContext context) - { - _context = context; - Projects = new ProjectRepository(_context); - Tickets = new TicketRepository(_context); - AppUsers = new AppUserRepository(_context); - } - - public IProjectRepository Projects { get; private set; } - - public IAppUserRepository AppUsers { get; private set; } - - public ITicketRepository Tickets { get; private set; } - - public async Task Complete() - { - return await _context.SaveChangesAsync(); - } - public void Dispose() - { - _context.DisposeAsync(); - } - } -} \ No newline at end of file diff --git a/Models/AppUser.cs b/Models/AppUser.cs index aa762d9..ceb557b 100644 --- a/Models/AppUser.cs +++ b/Models/AppUser.cs @@ -37,7 +37,7 @@ namespace TicketManager.Models public DateTime Created_at { get; private set; } = DateTime.Now; [Display(Name = "Avatar")] - public byte[] Picture { get; set; } + public string Picture { get; set; } public List Assignments { get; set; } = new List(); diff --git a/Models/Project.cs b/Models/Project.cs index fceedcf..7f1df5a 100644 --- a/Models/Project.cs +++ b/Models/Project.cs @@ -95,6 +95,10 @@ namespace TicketManager.Models public void RemoveMembers(List membersToRemove) { this.Assignments.RemoveAll(a => membersToRemove.Contains(a.User)); + + // membersToRemove.ForEach( + // m => m.Assignments.RemoveAll(a => (a.Project == this)) + // ); } public void SetMembers(List projectMembers) diff --git a/Models/Ticket.cs b/Models/Ticket.cs index 5b28ded..0063be7 100644 --- a/Models/Ticket.cs +++ b/Models/Ticket.cs @@ -47,7 +47,7 @@ namespace TicketManager.Models { return Project.Assignments.Select(a => a.User).ToList(); } - public void GetLastUpdateTime() { throw new NotImplementedException("Not Implemented"); } + public void GetLastUpdateTime() { throw new NotImplementedException("Not Implemented Yet."); } public void Close() { Status = Status.Done; diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 42e74be..7fb0bbb 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -20,7 +20,7 @@ "TicketManager": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "weatherforecast", + "launchUrl": "/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/README.md b/README.md index 17b770b..73fc768 100644 --- a/README.md +++ b/README.md @@ -47,3 +47,4 @@ - [ ] check useRef, useReducer, dispatch - [ ] error page redirect when offline. - [ ] ticket/files/activities list placeholders when empty +- [ ] think about public/private DTO's constructor, getters and setters diff --git a/Startup.cs b/Startup.cs index f86c247..a663213 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Reflection; +using System.IO; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpsPolicy; @@ -13,13 +12,10 @@ using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer; using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Models; -using System.Reflection; -using System.IO; -using TicketManager.Data; -using TicketManager.Models; using Microsoft.AspNetCore.Mvc.NewtonsoftJson; -using Newtonsoft.Json; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Newtonsoft.Json; +using TicketManager.Data; [assembly: ApiController] namespace TicketManager @@ -36,10 +32,14 @@ namespace TicketManager public void ConfigureServices(IServiceCollection services) { services.AddDbContext(options => - options.UseSqlite(Configuration.GetConnectionString("Sqlite"))); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + { + options.UseSqlite(Configuration.GetConnectionString("Sqlite")); + options.EnableSensitiveDataLogging(true); //Remove in production. + } + ); + // services.AddScoped(); + // services.AddScoped(); + // services.AddScoped(); services.AddAuthentication(options => { diff --git a/Tests/TicketManager.Tests/UnitTests/ControllersTests/ProjectControllerTests.cs b/Tests/TicketManager.Tests/UnitTests/ControllersTests/ProjectControllerTests.cs new file mode 100644 index 0000000..6bda2fb --- /dev/null +++ b/Tests/TicketManager.Tests/UnitTests/ControllersTests/ProjectControllerTests.cs @@ -0,0 +1,253 @@ +using System; +using Xunit; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Data.Sqlite; +using TicketManager.Controllers; +using TicketManager.Data; +using TicketManager.Models; +using TicketManager.DTO; + + +namespace TicketManager.Tests +{ + public class ProjectsControllerTests + { + [Fact] + public async Task Get_ReturnsListWith2Projects() + { + var connection = new SqliteConnection("DataSource=:memory:"); + connection.Open(); + + try + { + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + + // creates DB schema + using (var context = new AppDbContext(options)) + { + context.Database.EnsureCreated(); + } + + // Seed DB usng one context instance + SeedDb(options); + + using (var context = new AppDbContext(options)) + { + var controller = new ProjectsController(context); + + var result = await controller.GetProjects(); + + Assert.IsAssignableFrom>(result); + Assert.Equal(2, result.Count); + } + } + finally + { + connection.Close(); + } + } + + [Fact] + public async Task Get1_Returns1Project() + { + var connection = new SqliteConnection("DataSource=:memory:"); + connection.Open(); + + try + { + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + + // creates DB schema + using (var context = new AppDbContext(options)) + { + context.Database.EnsureCreated(); + } + + // Seed DB usng one context instance + SeedDb(options); + + using (var context = new AppDbContext(options)) + { + var controller = new ProjectsController(context); + + // Should Return 1 Project + var result = await controller.GetProject(1); + Assert.IsAssignableFrom(result); + + // Should Return NotFound + result = await controller.GetProject(3); + Assert.IsType(result); + } + } + finally + { + connection.Close(); + } + } + + [Fact] + public async Task Put1_Updates1Project() + { + // ControllersTests.Wrapper(Test_PutProject, SeedDb); + // } + + // private static async Task Test_PutProject() + // { + var connection = new SqliteConnection("DataSource=:memory:"); + connection.Open(); + + try + { + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + + // creates DB schema + using (var context = new AppDbContext(options)) + { + context.Database.EnsureCreated(); + } + + // Seed DB usng one context instance + SeedDb(options); + + using (var context = new AppDbContext(options)) + { + var controller = new ProjectsController(context); + + var result = await controller.PutProject(1, + new Project() + { + Id = 1, + Title = "Top Secret Project", + Description = "Shht Don't Ask don't tell", + PlannedEnding = new DateTime(2020, 7, 21) + } + ); + + // Should Update + Assert.Equal("Top Secret Project", context.Projects.Find(1).Title); + Assert.Equal(new DateTime(2020, 7, 21), context.Projects.Find(1).PlannedEnding); + Assert.IsType(result); + + + result = await controller.PutProject(2, + new Project() + { + Id = 1, + Title = "Top Secret Project", + Description = "Shht Don't Ask don't tell", + PlannedEnding = new DateTime(2020, 7, 21) + } + ); + + // Should Return BadRequest + Assert.NotEqual("Top Secret Project", context.Projects.Find(2).Title); + Assert.NotEqual(new DateTime(2020, 7, 21), context.Projects.Find(2).CreatedAt); + Assert.IsType(result); + + // Delete updated project + context.Projects.RemoveRange(context.Projects.Find(1)); + await context.SaveChangesAsync(); + + result = await controller.PutProject(1, + new Project() + { + Id = 1, + Title = "Top Secret Project", + Description = "Shht Don't Ask don't tell", + PlannedEnding = new DateTime(2020, 7, 21) + } + ); + + // Should Throw + Assert.IsType(result); + Assert.Equal("Top Secret Project", context.Projects.Find(1).Title); + Assert.Equal(new DateTime(2020, 7, 21), context.Projects.Find(1).PlannedEnding); + } + } + + finally + { + connection.Close(); + } + } + + [Fact] + public async Task Post_CreatesProject() + { + // Create inMemory Test Database + var connection = new SqliteConnection("DataSource=:memory:"); + connection.Open(); + + try + { + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + + // creates DB schema + using (var context = new AppDbContext(options)) + { + context.Database.EnsureCreated(); + } + + // Seed DB usng one context instance + SeedDb(options); + + // use another context instance to run the test + using (var context = new AppDbContext(options)) + { + var proj = new Project() + { + Title = "The Third", + Description = "Thrice in a row", + PlannedEnding = DateTime.Now + }; + + var controller = new ProjectsController(context); + + var result = await controller.PostProject(proj); + + Assert.IsAssignableFrom(result); + Assert.Equal(3, await context.Projects.CountAsync()); + } + } + finally + { + connection.Close(); + } + } + + private static void SeedDb(DbContextOptions options) + // Seed DB usng one context instance + { + using (var context = new AppDbContext(options)) + { + context.Projects.AddRange( + new Project() + { + Id = 1, + Title = "Secret Project", + Description = "Shht Don't Ask don't tell", + PlannedEnding = new DateTime(2021, 7, 21) + }, + new Project() + { + Id = 2, + Title = "Public Project", + Description = "It's quite obvious, isn't it?!", + PlannedEnding = new DateTime(2036, 6, 16) + }); + context.SaveChanges(); + } + } + } +} diff --git a/Tests/TicketManager.Tests/UnitTests/AppUserModelTests.cs b/Tests/TicketManager.Tests/UnitTests/ModelTests/AppUserModelTests.cs similarity index 100% rename from Tests/TicketManager.Tests/UnitTests/AppUserModelTests.cs rename to Tests/TicketManager.Tests/UnitTests/ModelTests/AppUserModelTests.cs index 8243ef1..2016528 100644 --- a/Tests/TicketManager.Tests/UnitTests/AppUserModelTests.cs +++ b/Tests/TicketManager.Tests/UnitTests/ModelTests/AppUserModelTests.cs @@ -50,6 +50,13 @@ namespace TicketManager.Tests Ticket t5 = new Ticket(); Ticket t6 = new Ticket(); + p1.Tickets.Add(t1); + p2.Tickets.Add(t2); + p2.Tickets.Add(t3); + p3.Tickets.Add(t4); + p3.Tickets.Add(t5); + p3.Tickets.Add(t6); + Assignment a1 = new Assignment() { User = user, @@ -69,13 +76,6 @@ namespace TicketManager.Tests }; user.Assignments.Add(a3); - p1.Tickets.Add(t1); - p2.Tickets.Add(t2); - p2.Tickets.Add(t3); - p3.Tickets.Add(t4); - p3.Tickets.Add(t5); - p3.Tickets.Add(t6); - var res = user.GetTickets().Count; Assert.Equal(6, res); } diff --git a/Tests/TicketManager.Tests/UnitTests/ProjectModelTests.cs b/Tests/TicketManager.Tests/UnitTests/ModelTests/ProjectModelTests.cs similarity index 100% rename from Tests/TicketManager.Tests/UnitTests/ProjectModelTests.cs rename to Tests/TicketManager.Tests/UnitTests/ModelTests/ProjectModelTests.cs diff --git a/Tests/TicketManager.Tests/UnitTests/TicketModelTests.cs b/Tests/TicketManager.Tests/UnitTests/ModelTests/TicketModelTests.cs similarity index 100% rename from Tests/TicketManager.Tests/UnitTests/TicketModelTests.cs rename to Tests/TicketManager.Tests/UnitTests/ModelTests/TicketModelTests.cs diff --git a/Tests/TicketManager.Tests/UnitTests/ProjectControllerTests.cs b/Tests/TicketManager.Tests/UnitTests/ProjectControllerTests.cs deleted file mode 100644 index 6e1d788..0000000 --- a/Tests/TicketManager.Tests/UnitTests/ProjectControllerTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Xunit; -using System.Collections.Generic; -using TicketManager.Controllers; -using TicketManager.Data; -using TicketManager.Models; - -namespace TicketManager.Tests -{ - public class ProjectsControllerTests - { - - - public ProjectsControllerTests() - { - // _context = context; - } - - // [Fact] - // public void Get_ReturnsProjectList() - // { - // // Arange - // // var controller = new ProjectsController(); - - // // Act - // // var result = controller.GetProjects(); - - // // Assert - // // Assert.IsType>(result); - // } - } -} diff --git a/TicketManager.csproj b/TicketManager.csproj index 812b03b..d68b951 100644 --- a/TicketManager.csproj +++ b/TicketManager.csproj @@ -3,7 +3,7 @@ netcoreapp3.1 - + 8.0 @@ -26,9 +26,14 @@ + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all +