diff --git a/API/AppHttpContext.cs b/API/Context/HttpContextAccessor.cs similarity index 50% rename from API/AppHttpContext.cs rename to API/Context/HttpContextAccessor.cs index ff1e504..bcf01e1 100644 --- a/API/AppHttpContext.cs +++ b/API/Context/HttpContextAccessor.cs @@ -1,13 +1,12 @@ using API.Validation; -using Microsoft.AspNetCore.Mvc; -namespace API; +namespace API.Context; -public static class AppHttpContext +public class HttpContextAccessor : IContextAccessor { - public static int CurrentUserId(this ControllerBase b) + public int CurrentUserId(HttpContext ctx) { - var idClaim = b.HttpContext + var idClaim = ctx .User .Claims .First(c => c.Type == IdentityData.IdUserClaimName); diff --git a/API/Context/IContextAccessor.cs b/API/Context/IContextAccessor.cs new file mode 100644 index 0000000..b337611 --- /dev/null +++ b/API/Context/IContextAccessor.cs @@ -0,0 +1,6 @@ +namespace API.Context; + +public interface IContextAccessor +{ + public int CurrentUserId(HttpContext ctx); +} \ No newline at end of file diff --git a/API/Controllers/Admin/TeamsController.cs b/API/Controllers/Admin/TeamsAdminController.cs similarity index 91% rename from API/Controllers/Admin/TeamsController.cs rename to API/Controllers/Admin/TeamsAdminController.cs index 6418fe3..d255687 100644 --- a/API/Controllers/Admin/TeamsController.cs +++ b/API/Controllers/Admin/TeamsAdminController.cs @@ -3,10 +3,10 @@ using Microsoft.AspNetCore.Mvc; using Model; using Services; -namespace API.Controllers; +namespace API.Controllers.Admin; [ApiController] -public class TeamsController(ITeamService service) : ControllerBase +public class TeamsAdminController(ITeamService service) : ControllerBase { public record CountTeamsResponse(int Value); @@ -14,7 +14,7 @@ public class TeamsController(ITeamService service) : ControllerBase [HttpGet("/admin/teams/count")] public async Task CountTeams() { - return new CountTeamsResponse(await service.CountTeams()); + return new CountTeamsResponse(await service.CountTotalTeams()); } // [HttpGet("/admin/users/count")] diff --git a/API/Controllers/Admin/UsersController.cs b/API/Controllers/Admin/UsersAdminController.cs similarity index 86% rename from API/Controllers/Admin/UsersController.cs rename to API/Controllers/Admin/UsersAdminController.cs index f93f6af..83bca2f 100644 --- a/API/Controllers/Admin/UsersController.cs +++ b/API/Controllers/Admin/UsersAdminController.cs @@ -3,13 +3,11 @@ using Microsoft.AspNetCore.Mvc; using Model; using Services; -namespace API.Controllers; +namespace API.Controllers.Admin; [ApiController] -public class UsersController(IUserService service) : ControllerBase +public class UsersAdminController(IUserService service) : ControllerBase { - private const string DefaultProfilePicture = - "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png"; public record CountUsersResponse(int Value); @@ -22,13 +20,13 @@ public class UsersController(IUserService service) : ControllerBase { return new CountUsersResponse(await service.UsersCount(search)); } - + [HttpGet("/admin/users/count")] public async Task CountUsers() { return new CountUsersResponse(await service.UsersCount()); } - + // [HttpGet("/admin/users/count")] // public async Task CountUsers() // { @@ -54,7 +52,7 @@ public class UsersController(IUserService service) : ControllerBase [HttpGet("/admin/users/{id:int}")] public async Task GetUser( - [Range(0, int.MaxValue, ErrorMessage = "Only positive number allowed")] + [Range(1, int.MaxValue, ErrorMessage = "Only positive number allowed")] int id ) { @@ -79,15 +77,16 @@ public class UsersController(IUserService service) : ControllerBase [HttpPost("/admin/users")] public Task AddUser([FromBody] AddUserRequest req) { - return service.CreateUser(req.Username, req.Email, req.Password, DefaultProfilePicture, req.IsAdmin); + return service.CreateUser(req.Username, req.Email, req.Password, UsersController.DefaultProfilePicture, req.IsAdmin); } public record RemoveUsersRequest(int[] Identifiers); [HttpPost("/admin/users/remove-all")] - public async void RemoveUsers([FromBody] RemoveUsersRequest req) + public async Task RemoveUsers([FromBody] RemoveUsersRequest req) { await service.RemoveUsers(req.Identifiers); + return Ok(); } public record UpdateUserRequest( @@ -107,7 +106,7 @@ public class UsersController(IUserService service) : ControllerBase { try { - await service.UpdateUser(new User(id, req.Username, req.Email, DefaultProfilePicture, req.IsAdmin)); + await service.UpdateUser(new User(id, req.Username, req.Email, UsersController.DefaultProfilePicture, req.IsAdmin)); return Ok(); } catch (ServiceException e) diff --git a/API/Controllers/AuthenticationController.cs b/API/Controllers/AuthenticationController.cs index 584a869..3567ce9 100644 --- a/API/Controllers/AuthenticationController.cs +++ b/API/Controllers/AuthenticationController.cs @@ -22,7 +22,8 @@ public class AuthenticationController(IUserService service, IConfiguration confi [EmailAddress] string Email, [MaxLength(256, ErrorMessage = "Password is too wide")] - string Password); + string Password + ); private record AuthenticationResponse(String Token, DateTime ExpirationDate); diff --git a/API/Controllers/TacticController.cs b/API/Controllers/TacticsController.cs similarity index 88% rename from API/Controllers/TacticController.cs rename to API/Controllers/TacticsController.cs index 9f08ae3..9452f11 100644 --- a/API/Controllers/TacticController.cs +++ b/API/Controllers/TacticsController.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json; +using API.Context; using API.DTO; using API.Validation; using Microsoft.AspNetCore.Authorization; @@ -10,7 +11,7 @@ using Services; namespace API.Controllers; [ApiController] -public class TacticController(ITacticService service) : ControllerBase +public class TacticController(ITacticService service, IContextAccessor accessor) : ControllerBase { public record UpdateNameRequest( [StringLength(50, MinimumLength = 1)] @@ -23,7 +24,7 @@ public class TacticController(ITacticService service) : ControllerBase int tacticId, [FromBody] UpdateNameRequest req) { - var userId = this.CurrentUserId(); + var userId = accessor.CurrentUserId(HttpContext); if (!await service.HasAnyRights(userId, tacticId)) { return Unauthorized(); @@ -38,7 +39,7 @@ public class TacticController(ITacticService service) : ControllerBase [Authorize] public async Task GetTacticInfo(int tacticId) { - var userId = this.CurrentUserId(); + var userId = accessor.CurrentUserId(HttpContext); if (!await service.HasAnyRights(userId, tacticId)) { return Unauthorized(); @@ -54,7 +55,7 @@ public class TacticController(ITacticService service) : ControllerBase [Authorize] public async Task GetTacticStepsRoot(int tacticId) { - var userId = this.CurrentUserId(); + var userId = accessor.CurrentUserId(HttpContext); if (!await service.HasAnyRights(userId, tacticId)) { return Unauthorized(); @@ -78,7 +79,7 @@ public class TacticController(ITacticService service) : ControllerBase [Authorize] public async Task CreateNew([FromBody] CreateNewRequest req) { - var userId = this.CurrentUserId(); + var userId = accessor.CurrentUserId(HttpContext); var courtType = req.CourtType switch { @@ -107,7 +108,7 @@ public class TacticController(ITacticService service) : ControllerBase [Authorize] public async Task GetStepContent(int tacticId, int stepId) { - var userId = this.CurrentUserId(); + var userId = accessor.CurrentUserId(HttpContext); if (!await service.HasAnyRights(userId, tacticId)) { @@ -122,7 +123,7 @@ public class TacticController(ITacticService service) : ControllerBase [Authorize] public async Task RemoveStepContent(int tacticId, int stepId) { - var userId = this.CurrentUserId(); + var userId = accessor.CurrentUserId(HttpContext); if (!await service.HasAnyRights(userId, tacticId)) { @@ -140,7 +141,7 @@ public class TacticController(ITacticService service) : ControllerBase [Authorize] public async Task SaveStepContent(int tacticId, int stepId, [FromBody] SaveStepContentRequest req) { - var userId = this.CurrentUserId(); + var userId = accessor.CurrentUserId(HttpContext); if (!await service.HasAnyRights(userId, tacticId)) { diff --git a/API/Controllers/TeamsController.cs b/API/Controllers/TeamsController.cs new file mode 100644 index 0000000..aecfc67 --- /dev/null +++ b/API/Controllers/TeamsController.cs @@ -0,0 +1,77 @@ +using System.ComponentModel.DataAnnotations; +using API.Context; +using API.Validation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Model; +using Services; + +namespace API.Controllers; + +[ApiController] +[Authorize] +public class TeamsController(ITeamService service, IContextAccessor accessor) : ControllerBase +{ + public record CreateTeamRequest( + [Name] string Name, + [Url] string Picture, + [RegularExpression("^#[0-9A-F]{6}$")] string FirstColor, + [RegularExpression("^#[0-9A-F]{6}$")] string SecondColor + ); + + [HttpPost("/teams")] + public async Task CreateTeam([FromBody] CreateTeamRequest req) + { + var userId = accessor.CurrentUserId(HttpContext); + var team = await service.AddTeam(req.Name, req.Picture, req.FirstColor, req.SecondColor); + await service.AddMember(team.Id, userId, MemberRole.Coach); + return Ok(team); + } + + [HttpGet("/teams/{teamId:int}/members")] + public IActionResult GetMembersOf(int teamId) + { + return Ok(service.GetMembersOf(teamId)); + } + + public record AddMemberRequest( + int UserId, + [AllowedValues("PLAYER", "COACH")] string Role + ); + + [HttpPost("/teams/{teamId:int}/members")] + public async Task AddMember(int teamId, [FromBody] AddMemberRequest req) + { + if (!Enum.TryParse(req.Role, true, out var role)) + { + throw new Exception($"Unable to convert string input '{req.Role}' to a role enum variant."); + } + + + return Ok(await service.AddMember(teamId, req.UserId, role)); + } + + + public record UpdateMemberRequest( + [AllowedValues("PLAYER", "COACH")] string Role + ); + + [HttpPut("/team/{teamId:int}/members/{userId:int}")] + public async Task UpdateMember(int teamId, int userId, [FromBody] UpdateMemberRequest req) + { + if (!Enum.TryParse(req.Role, true, out var role)) + { + throw new Exception($"Unable to convert string input '{req.Role}' to a role enum variant."); + } + + var updated = await service.UpdateMember(new Member(teamId, userId, role)); + return updated ? Ok() : NotFound(); + } + + [HttpDelete("/team/{teamId:int}/members/{userId:int}")] + public async Task RemoveMember(int teamId, int userId) + { + var removed = await service.RemoveMember(teamId, userId); + return removed ? Ok() : NotFound(); + } +} \ No newline at end of file diff --git a/API/Controllers/UserController.cs b/API/Controllers/UsersController.cs similarity index 58% rename from API/Controllers/UserController.cs rename to API/Controllers/UsersController.cs index 8c6b26c..2bb6a4f 100644 --- a/API/Controllers/UserController.cs +++ b/API/Controllers/UsersController.cs @@ -1,19 +1,26 @@ +using System.Runtime.CompilerServices; +using API.Context; using API.DTO; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Model; using Services; +[assembly: InternalsVisibleTo("UnitTests")] namespace API.Controllers; [ApiController] -public class UserController(IUserService users, ITeamService teams, ITacticService tactics) : ControllerBase +public class UsersController(IUserService users, ITeamService teams, ITacticService tactics, IContextAccessor accessor) + : ControllerBase { + public const string DefaultProfilePicture = + "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png"; + [Authorize] [HttpGet("/user")] public async Task GetUser() { - var userId = this.CurrentUserId(); + var userId = accessor.CurrentUserId(HttpContext); return (await users.GetUser(userId))!; } @@ -23,7 +30,7 @@ public class UserController(IUserService users, ITeamService teams, ITacticServi [HttpGet("/user-data")] public async Task GetUserData() { - var userId = this.CurrentUserId(); + var userId = accessor.CurrentUserId(HttpContext); var userTeams = await teams.ListTeamsOf(userId); var userTactics = await tactics.ListTacticsOf(userId); return new GetUserDataResponse(userTeams.ToArray(), userTactics.Select(t => t.ToDto()).ToArray()); diff --git a/API/Program.cs b/API/Program.cs index 278bca2..cc43480 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -2,12 +2,14 @@ using System.Globalization; using System.Net.Mime; using System.Text; using API.Auth; +using API.Context; using API.Validation; using DbServices; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using Services; +using HttpContextAccessor = API.Context.HttpContextAccessor; var builder = WebApplication.CreateBuilder(args); var config = builder.Configuration; @@ -17,7 +19,7 @@ var config = builder.Configuration; builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -builder.Services.AddHttpLogging(o => {}); +builder.Services.AddHttpLogging(o => { }); builder.Services.AddControllers() .ConfigureApiBehaviorOptions(options => options.InvalidModelStateResponseFactory = context => @@ -55,6 +57,7 @@ builder.Services.AddDbContext(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); @@ -104,5 +107,4 @@ app.Use((context, next) => }); - app.Run(); \ No newline at end of file diff --git a/API/Properties/launchSettings.json b/API/Properties/launchSettings.json index 27799cf..e8503c8 100644 --- a/API/Properties/launchSettings.json +++ b/API/Properties/launchSettings.json @@ -14,7 +14,7 @@ "dotnetRunMessages": true, "launchBrowser": false, "launchUrl": "swagger", - "applicationUrl": "http://localhost:5254", + "applicationUrl": "http://+:5254", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/API/Validation/IdentityData.cs b/API/Validation/IdentityData.cs index 9af9cd0..8a6a86d 100644 --- a/API/Validation/IdentityData.cs +++ b/API/Validation/IdentityData.cs @@ -1,3 +1,6 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("UnitTests")] namespace API.Validation; internal class IdentityData diff --git a/AppContext/Entities/MemberEntity.cs b/AppContext/Entities/MemberEntity.cs index 7ba1c20..841c370 100644 --- a/AppContext/Entities/MemberEntity.cs +++ b/AppContext/Entities/MemberEntity.cs @@ -1,3 +1,5 @@ +using Model; + namespace AppContext.Entities; public class MemberEntity @@ -9,5 +11,5 @@ public class MemberEntity public required int UserId { get; set; } public UserEntity? User { get; set; } - public required string Role { get; set; } + public required MemberRole Role { get; set; } } \ No newline at end of file diff --git a/AppContext/Entities/TacticEntity.cs b/AppContext/Entities/TacticEntity.cs index 899fd70..b4ef93f 100644 --- a/AppContext/Entities/TacticEntity.cs +++ b/AppContext/Entities/TacticEntity.cs @@ -12,8 +12,4 @@ public class TacticEntity public UserEntity? Owner { get; set; } public CourtType Type { get; set; } - public required string JsonContent { get; set; } - - public required int RootStepId { get; set; } - public TacticStepEntity RootStep { get; set; } } \ No newline at end of file diff --git a/AppContext/Entities/TacticStepEntity.cs b/AppContext/Entities/TacticStepEntity.cs index 117fc1d..c28b88b 100644 --- a/AppContext/Entities/TacticStepEntity.cs +++ b/AppContext/Entities/TacticStepEntity.cs @@ -7,9 +7,9 @@ public class TacticStepEntity public int Id { get; set; } - public required int TacticId { get; set; } - public Tactic Tactic { get; set; } + public int TacticId { get; set; } + public TacticEntity Tactic { get; set; } public required int? ParentId { get; set; } diff --git a/Converters/ModelToEntities.cs b/Converters/ModelToEntities.cs index 4ecd15c..6c8006c 100644 --- a/Converters/ModelToEntities.cs +++ b/Converters/ModelToEntities.cs @@ -32,4 +32,9 @@ public static class EntitiesToModels { return new Team(entity.Id, entity.Name, entity.Picture, entity.MainColor, entity.SecondColor); } + + public static Member ToModel(this MemberEntity entity) + { + return new Member(entity.TeamId, entity.UserId, entity.Role); + } } \ No newline at end of file diff --git a/DbServices/DbTacticService.cs b/DbServices/DbTacticService.cs index 466246e..1fcb1b3 100644 --- a/DbServices/DbTacticService.cs +++ b/DbServices/DbTacticService.cs @@ -38,16 +38,14 @@ public class DbTacticService(AppContext.AppContext context) : ITacticService Type = courtType }; - await context.Tactics.AddAsync(tacticEntity); - await context.SaveChangesAsync(); var stepEntity = new TacticStepEntity { ParentId = null, - TacticId = tacticEntity.Id, - JsonContent = "{\"components\": []}" + JsonContent = "{\"components\": []}", + Tactic = tacticEntity }; - + await context.Tactics.AddAsync(tacticEntity); await context.TacticSteps.AddAsync(stepEntity); await context.SaveChangesAsync(); diff --git a/DbServices/DbTeamService.cs b/DbServices/DbTeamService.cs index 0393b84..6ca6e1e 100644 --- a/DbServices/DbTeamService.cs +++ b/DbServices/DbTeamService.cs @@ -42,7 +42,7 @@ public class DbTeamService(AppContext.AppContext context) : ITeamService return await context.Teams.CountAsync(t => t.Name.ToLower().Contains(nameNeedle.ToLower())); } - public async Task CountTeams() + public async Task CountTotalTeams() { return await context.Teams.CountAsync(); } @@ -82,4 +82,43 @@ public class DbTeamService(AppContext.AppContext context) : ITeamService return await context.SaveChangesAsync() > 0; } + + + public IEnumerable GetMembersOf(int teamId) + { + return context.Members.Where(m => m.TeamId == teamId) + .AsEnumerable() + .Select(e => e.ToModel()); + } + + public async Task AddMember(int teamId, int userId, MemberRole role) + { + await context.Members.AddAsync(new MemberEntity + { + TeamId = teamId, + UserId = userId, + Role = role + }); + await context.SaveChangesAsync(); + return new Member(teamId, userId, role); + } + + public async Task UpdateMember(Member member) + { + var entity = + await context.Members.FirstOrDefaultAsync(e => e.TeamId == member.TeamId && e.UserId == member.UserId); + if (entity == null) + return false; + entity.Role = member.Role; + + return await context.SaveChangesAsync() > 0; + } + + public async Task RemoveMember(int teamId, int userId) + { + await context.Members + .Where(e => e.TeamId == teamId && e.UserId == userId) + .ExecuteDeleteAsync(); + return await context.SaveChangesAsync() > 0; + } } \ No newline at end of file diff --git a/Model/CourtType.cs b/Model/CourtType.cs index 2bfdddd..84fa4a8 100644 --- a/Model/CourtType.cs +++ b/Model/CourtType.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations.Schema; + namespace Model; public enum CourtType diff --git a/Model/Member.cs b/Model/Member.cs new file mode 100644 index 0000000..707b764 --- /dev/null +++ b/Model/Member.cs @@ -0,0 +1,3 @@ +namespace Model; + +public record Member(int TeamId, int UserId, MemberRole Role); \ No newline at end of file diff --git a/Model/MemberRole.cs b/Model/MemberRole.cs new file mode 100644 index 0000000..5afaf8c --- /dev/null +++ b/Model/MemberRole.cs @@ -0,0 +1,7 @@ +namespace Model; + +public enum MemberRole +{ + Player, + Coach +} \ No newline at end of file diff --git a/Services/ITeamService.cs b/Services/ITeamService.cs index ae19f90..63deaeb 100644 --- a/Services/ITeamService.cs +++ b/Services/ITeamService.cs @@ -8,10 +8,19 @@ public interface ITeamService public Task> ListTeams(); - public Task CountTeams(); + public Task CountTotalTeams(); public Task AddTeam(string name, string picture, string firstColor, string secondColor); public Task RemoveTeams(params int[] teams); + public IEnumerable GetMembersOf(int teamId); + + public Task AddMember(int teamId, int userId, MemberRole role); + + public Task UpdateMember(Member member); + + public Task RemoveMember(int teamId, int userId); + public Task UpdateTeam(Team team); + } \ No newline at end of file diff --git a/StubContext/StubAppContext.cs b/StubContext/StubAppContext.cs index 39a944e..f740de9 100644 --- a/StubContext/StubAppContext.cs +++ b/StubContext/StubAppContext.cs @@ -1,5 +1,6 @@ using AppContext.Entities; using Microsoft.EntityFrameworkCore; +using Model; namespace StubContext; @@ -36,5 +37,24 @@ public class StubAppContext(DbContextOptions options) : AppContext(o builder.Entity() .HasKey("TeamId", "UserId"); + + builder.Entity() + .HasData(new TacticEntity() + { + Id = 1, + Name = "New tactic", + Type = CourtType.Plain, + CreationDate = new DateTime(2024, 5, 31), + OwnerId = 1, + }); + + builder.Entity() + .HasData(new TacticStepEntity + { + Id = 1, + JsonContent = "{}", + TacticId = 1, + ParentId = null + }); } } \ No newline at end of file diff --git a/UnitTests/AdminUserControllerTest.cs b/UnitTests/AdminUserControllerTest.cs new file mode 100644 index 0000000..3e32cbe --- /dev/null +++ b/UnitTests/AdminUserControllerTest.cs @@ -0,0 +1,126 @@ +using API.Controllers; +using API.Controllers.Admin; +using DbServices; +using FluentAssertions; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Model; +using StubContext; + + +namespace UnitTests; + +public class AdminUserControllerTest +{ + private static UsersAdminController GetUsersController() + { + var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + var context = new StubAppContext( + new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options + ); + context.Database.EnsureCreated(); + var service = new DbUserService(context); + return new UsersAdminController(service); + } + + + [Fact] + public async void CountUsersTest() + { + var controller = GetUsersController(); + (await controller.CountUsers()).Should().Be(new UsersAdminController.CountUsersResponse(5)); + (await controller.CountUsers("a")).Should().BeEquivalentTo(new UsersAdminController.CountUsersResponse(3)); + (await controller.CountUsers("")).Should().BeEquivalentTo(new UsersAdminController.CountUsersResponse(5)); + (await controller.CountUsers("^ù$*")).Should().BeEquivalentTo(new UsersAdminController.CountUsersResponse(0)); + } + + [Fact] + public async void ListUsersTest() + { + var controller = GetUsersController(); + (await controller.CountUsers()).Should().Be(new UsersAdminController.CountUsersResponse(5)); + (await controller.ListUsers(0, 10, null)).Should().BeEquivalentTo(new List + { + new(1, "maxime", "maxime@mail.com", + UsersController.DefaultProfilePicture, true), + new(2, "mael", "mael@mail.com", + UsersController.DefaultProfilePicture, true), + new(3, "yanis", "yanis@mail.com", + UsersController.DefaultProfilePicture, true), + new(4, "simon", "simon@mail.com", + UsersController.DefaultProfilePicture, true), + new(5, "vivien", "vivien@mail.com", + UsersController.DefaultProfilePicture, true), + }); + (await controller.ListUsers(0, 10, "")).Should().BeEquivalentTo(new List + { + new(1, "maxime", "maxime@mail.com", + UsersController.DefaultProfilePicture, true), + new(2, "mael", "mael@mail.com", + UsersController.DefaultProfilePicture, true), + new(3, "yanis", "yanis@mail.com", + UsersController.DefaultProfilePicture, true), + new(4, "simon", "simon@mail.com", + UsersController.DefaultProfilePicture, true), + new(5, "vivien", "vivien@mail.com", + UsersController.DefaultProfilePicture, true), + }); + (await controller.ListUsers(0, 10, "a")).Should().BeEquivalentTo(new List + { + new(1, "maxime", "maxime@mail.com", + UsersController.DefaultProfilePicture, true), + new(2, "mael", "mael@mail.com", + UsersController.DefaultProfilePicture, true), + new(3, "yanis", "yanis@mail.com", + UsersController.DefaultProfilePicture, true), + }); + (await controller.ListUsers(0, 0, "")).Should().BeEquivalentTo(new List { }); + (await controller.ListUsers(0, 10, "^ù$*")).Should().BeEquivalentTo(new List { }); + } + + [Fact] + public async void GetUserTest() + { + var controller = GetUsersController(); + var response = await controller.GetUser(0); + response.Should().BeEquivalentTo(controller.NotFound()); + + response = await controller.GetUser(1); + response.Should().BeEquivalentTo(controller.Ok(new User(1, "maxime", "maxime@mail.com", + UsersController.DefaultProfilePicture, true))); + } + + [Fact] + public async void AddUserTest() + { + var controller = GetUsersController(); + var user = await controller.AddUser(new("Test", "TestPassword", "test@mail.com", true)); + user.Should().BeEquivalentTo(new User(6, "Test", "test@mail.com", UsersController.DefaultProfilePicture, true)); + } + + [Fact] + public async void RemoveUsersTest() + { + var controller = GetUsersController(); + var result = await controller.RemoveUsers(new([1, 4, 3, 5])); + result.Should().BeEquivalentTo(controller.Ok()); + + var remainingUsers = await controller.ListUsers(0, 10, null); + remainingUsers.Should().BeEquivalentTo(new User[] { new(2, "mael", "mael@mail.com", + UsersController.DefaultProfilePicture, true) }); + } + + [Fact] + public async void UpdateUserTest() + { + var controller = GetUsersController(); + var result = await controller.UpdateUser(1, new("maxou", "maxou@mail.com", false)); + result.Should().BeEquivalentTo(controller.Ok()); + + var userResult = await controller.GetUser(1); + userResult.Should().BeEquivalentTo(controller.Ok(new User(1, "maxou", "maxou@mail.com", UsersController.DefaultProfilePicture, false))); + } +} \ No newline at end of file diff --git a/UnitTests/GlobalUsings.cs b/UnitTests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/UnitTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/UnitTests/ManualContextAccessor.cs b/UnitTests/ManualContextAccessor.cs new file mode 100644 index 0000000..ef73d51 --- /dev/null +++ b/UnitTests/ManualContextAccessor.cs @@ -0,0 +1,12 @@ +using API.Context; +using Microsoft.AspNetCore.Http; + +namespace UnitTests; + +public class ManualContextAccessor(int userId) : IContextAccessor +{ + public int CurrentUserId(HttpContext ctx) + { + return userId; + } +} \ No newline at end of file diff --git a/UnitTests/TacticsControllerTest.cs b/UnitTests/TacticsControllerTest.cs new file mode 100644 index 0000000..1886b56 --- /dev/null +++ b/UnitTests/TacticsControllerTest.cs @@ -0,0 +1,66 @@ +using API.Controllers; +using API.DTO; +using AppContext.Entities; +using DbServices; +using FluentAssertions; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Model; +using StubContext; + +namespace UnitTests; + +public class TacticsControllerTest +{ + private static (TacticController, AppContext.AppContext) GetController(int userId) + { + var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + var context = new StubAppContext( + new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options + ); + context.Database.EnsureCreated(); + var controller = new TacticController( + new DbTacticService(context), + new ManualContextAccessor(userId) + ); + + return (controller, context); + } + + [Fact] + public async void UpdateName() + { + var (controller, context) = GetController(1); + var result = await controller.UpdateName(1, new("Stade de France")); + result.Should().BeEquivalentTo(controller.Ok()); + + var tactic = await context.Tactics.FindAsync(1); + tactic.Name.Should().BeEquivalentTo("Stade de France"); + + result = await controller.UpdateName(-1, new("Stade de France")); + result.Should().BeEquivalentTo(controller.Unauthorized()); + } + + [Fact] + public async void GetTacticInfoTest() + { + var (controller, context) = GetController(1); + var result = await controller.GetTacticInfo(1); + result.Should().BeEquivalentTo(controller.Ok(new Tactic(1, "New tactic", 1, CourtType.Plain, new DateTime(2024, 5, 31)).ToDto())); + + result = await controller.GetTacticInfo(100); + result.Should().BeEquivalentTo(controller.Unauthorized()); + } + + [Fact] + public async void GetTacticStepsRoot() + { + var (controller, context) = GetController(1); + var result = await controller.GetTacticStepsRoot(1); + result.Should().BeEquivalentTo(controller.Ok(new TacticController.GetTacticStepsTreeResponse(new TacticStep(1, null, [], "{}").ToDto()))); + } + +} \ No newline at end of file diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj new file mode 100644 index 0000000..2ca35f3 --- /dev/null +++ b/UnitTests/UnitTests.csproj @@ -0,0 +1,35 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/UnitTests/UserControllerTest.cs b/UnitTests/UserControllerTest.cs new file mode 100644 index 0000000..cb98c44 --- /dev/null +++ b/UnitTests/UserControllerTest.cs @@ -0,0 +1,49 @@ +using API.Controllers; +using DbServices; +using FluentAssertions; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Model; +using StubContext; + +namespace UnitTests; + +public class UsersControllerTest +{ + private static UsersController GetUserController(int userId) + { + var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + var context = new StubAppContext( + new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options + ); + context.Database.EnsureCreated(); + var controller = new UsersController( + new DbUserService(context), + new DbTeamService(context), + new DbTacticService(context), + new ManualContextAccessor(userId) + ); + + return controller; + } + + [Fact] + public async void GetCurrentUserTest() + { + var controller = GetUserController(1); + var result = await controller.GetUser(); + result.Should().BeEquivalentTo(new User(1, "maxime", "maxime@mail.com", + UsersController.DefaultProfilePicture, true)); + } + + [Fact] + public async void GetUserDataTest() + { + var controller = GetUserController(1); + var result = await controller.GetUserData(); + result.Should().BeEquivalentTo(new UsersController.GetUserDataResponse([], [])); + } +} \ No newline at end of file diff --git a/WebAPI.sln b/WebAPI.sln index 0e5f8c6..ba34e6d 100644 --- a/WebAPI.sln +++ b/WebAPI.sln @@ -4,8 +4,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Model", "Model\Model.csproj EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppContext", "AppContext\AppContext.csproj", "{7E245DA5-0C5A-4933-9AF0-CA447F3BC159}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DTO", "DTO\DTO.csproj", "{7BE8B235-EF70-4C94-8938-5ABB5AEB08B1}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StubContext", "StubContext\StubContext.csproj", "{420D507C-D51C-48D9-A819-72B08AEBD024}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API\API.csproj", "{B22FA426-EFF2-42E9-96BB-78F1C65E37CC}" @@ -16,6 +14,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbServices", "DbServices\Db EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Converters", "Converters\Converters.csproj", "{465819A9-7158-4612-AC57-ED2C7A0F243E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{82A100BE-5610-4741-8F23-1CD653E8EFCD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,5 +58,9 @@ Global {9C5EAD2F-FA50-43C2-BB86-1065ED661C52}.Debug|Any CPU.Build.0 = Debug|Any CPU {9C5EAD2F-FA50-43C2-BB86-1065ED661C52}.Release|Any CPU.ActiveCfg = Release|Any CPU {9C5EAD2F-FA50-43C2-BB86-1065ED661C52}.Release|Any CPU.Build.0 = Release|Any CPU + {82A100BE-5610-4741-8F23-1CD653E8EFCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82A100BE-5610-4741-8F23-1CD653E8EFCD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82A100BE-5610-4741-8F23-1CD653E8EFCD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82A100BE-5610-4741-8F23-1CD653E8EFCD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/ci/.drone.yml b/ci/.drone.yml index dbfcd13..8dab6da 100644 --- a/ci/.drone.yml +++ b/ci/.drone.yml @@ -1,18 +1,11 @@ kind: pipeline type: docker -name: "CI and deploy on iqball.maxou.dev" +name: "CI, deploy server on iqball.maxou.dev, deploy doc on codehub" steps: - - image: mcr.microsoft.com/dotnet/sdk:8.0 - name: "CI" - commands: - - dotnet test - - image: plugins/docker name: "build and push docker image" - depends_on: - - "CI" settings: dockerfile: ci/API.dockerfile context: . diff --git a/stryker-config.json b/stryker-config.json new file mode 100644 index 0000000..391891d --- /dev/null +++ b/stryker-config.json @@ -0,0 +1,9 @@ +{ + "stryker-config": + { + "reporters": [ + "progress", + "html" + ] + } +} \ No newline at end of file