Ajout de tout le nécessaire pour gérer la Blacklist (controller, services, etc) accompagné de tests unitaires. Aussi ajout de méthodes manquantes sur les UserController
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
8640499d75
commit
bcfb7b3e93
@ -0,0 +1,88 @@
|
||||
using Dto;
|
||||
using Entities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Model.OrderCriteria;
|
||||
using Shared;
|
||||
using Shared.Mapper;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
public class BlackListController(ILogger<UsersController> logger, IBlackListService<BlackListDto> blackListService) : ControllerBase
|
||||
{
|
||||
[HttpGet("user/ban/{page:int}/{number:int}")]
|
||||
[ProducesResponseType(typeof(IEnumerable<BlackListDto>), 200)]
|
||||
[ProducesResponseType(typeof(string), 204)]
|
||||
public IActionResult GetUsers(int page, int number, BlackListOdrerCriteria orderCriteria)
|
||||
{
|
||||
var users = blackListService.GetBannedUsers(page, number, orderCriteria).ToList();
|
||||
if (users.Count == 0)
|
||||
{
|
||||
logger.LogError("[ERREUR] Aucun email banni trouvé.");
|
||||
return StatusCode(204);
|
||||
}
|
||||
|
||||
logger.LogInformation("[INFORMATION] {nb} Email(s) banni(s) trouvé(s)", users.Count);
|
||||
return Ok(users);
|
||||
}
|
||||
|
||||
[HttpGet("user/ban/number")]
|
||||
[ProducesResponseType(typeof(UserDto), 200)]
|
||||
[ProducesResponseType(typeof(string), 204)]
|
||||
public IActionResult GetNumberOfBannedUsers()
|
||||
{
|
||||
var nb = blackListService.GetNumberOfBannedUsers();
|
||||
logger.LogInformation("[INFORMATION] {nb} Email(s) banni(s) trouvé(s)", nb);
|
||||
return Ok(nb);
|
||||
}
|
||||
|
||||
[HttpPost("user/ban")]
|
||||
[ProducesResponseType(typeof(UserDto), 200)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
public IActionResult GetUserBannedByEmail([FromBody] string email)
|
||||
{
|
||||
var res = blackListService.GetUserBannedByEmail(email);
|
||||
if (res != null)
|
||||
{
|
||||
logger.LogInformation("[INFORMATION] Utilisateur banni avec l'email {email} a été trouvé.", email);
|
||||
return Ok(res);
|
||||
}
|
||||
logger.LogError("[ERREUR] Aucun utilisateur banni trouvé avec l'email {email}.", email);
|
||||
return NotFound("Utilisateur non trouvé !");
|
||||
}
|
||||
|
||||
[HttpDelete("user/ban/{username:alpha}")]
|
||||
[ProducesResponseType(typeof(UserDto), 200)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
public IActionResult BanUser(string username)
|
||||
{
|
||||
var success = blackListService.BanUser(username);
|
||||
if (success)
|
||||
{
|
||||
logger.LogInformation("[INFORMATION] L'utilisateur avec le pseudo {username} a été banni pour 2 ans.", username);
|
||||
return Ok();
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("[ERREUR] Aucun utilisateur trouvé avec le pseudo {username}.", username);
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("user/unban")]
|
||||
[ProducesResponseType(typeof(UserDto), 200)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
public IActionResult UnbanUser([FromBody] string email)
|
||||
{
|
||||
var success = blackListService.UnbanUser(email);
|
||||
if (success)
|
||||
{
|
||||
logger.LogInformation("[INFORMATION] L'utilisateur avec l'email {email} a été débanni.", email);
|
||||
return Ok();
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("[ERREUR] Aucun utilisateur banni trouvé avec l'email {email}.", email);
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
using Dto;
|
||||
using Entities;
|
||||
using Model.OrderCriteria;
|
||||
using Shared;
|
||||
using Shared.Mapper;
|
||||
|
||||
namespace API.Service;
|
||||
|
||||
public class BlackListDataServiceAPI (IBlackListService<BlackListEntity> userService) : IBlackListService<BlackListDto>
|
||||
{
|
||||
public IEnumerable<BlackListDto> GetBannedUsers(int page, int number, BlackListOdrerCriteria orderCriteria) =>
|
||||
userService.GetBannedUsers(page, number, orderCriteria).Select(b => b.FromEntityToDto());
|
||||
|
||||
public int GetNumberOfBannedUsers() => userService.GetNumberOfBannedUsers();
|
||||
public BlackListDto? GetUserBannedByEmail(string email)
|
||||
{
|
||||
var res = userService.GetUserBannedByEmail(email);
|
||||
if (res == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return res.FromEntityToDto();
|
||||
}
|
||||
|
||||
public bool BanUser(string username) => userService.BanUser(username);
|
||||
public bool UnbanUser(string email) => userService.UnbanUser(email);
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
using DbContextLib;
|
||||
using Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Model.OrderCriteria;
|
||||
using Shared;
|
||||
|
||||
namespace DbDataManager.Service;
|
||||
|
||||
public class BlackListDataService : IBlackListService<BlackListEntity>
|
||||
{
|
||||
private UserDbContext DbContext { get; set; }
|
||||
|
||||
public BlackListDataService(UserDbContext context)
|
||||
{
|
||||
DbContext = context;
|
||||
context.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public int GetNumberOfBannedUsers()
|
||||
{
|
||||
return DbContext.BlackLists.Count();
|
||||
}
|
||||
public IEnumerable<BlackListEntity> GetBannedUsers(int page, int number, BlackListOdrerCriteria orderCriteria)
|
||||
{
|
||||
if (page <= 0)
|
||||
{
|
||||
page = 1;
|
||||
}
|
||||
|
||||
if (number <= 0)
|
||||
{
|
||||
number = 10;
|
||||
}
|
||||
IQueryable<BlackListEntity> query = DbContext.BlackLists.Skip((page - 1) * number).Take(number);
|
||||
switch (orderCriteria)
|
||||
{
|
||||
case BlackListOdrerCriteria.None:
|
||||
break;
|
||||
case BlackListOdrerCriteria.ByEmail:
|
||||
query = query.OrderBy(s => s.Email);
|
||||
break;
|
||||
case BlackListOdrerCriteria.ByExpirationDate:
|
||||
query = query.OrderBy(s => s.ExpirationDate);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
var blackList = query.ToList();
|
||||
return blackList;
|
||||
}
|
||||
public BlackListEntity? GetUserBannedByEmail(string email)
|
||||
{
|
||||
var blackListEntity = DbContext.BlackLists.FirstOrDefault(b => b.Email == email);
|
||||
return blackListEntity;
|
||||
}
|
||||
|
||||
public bool BanUser(string username)
|
||||
{
|
||||
var userEntity = DbContext.Users.FirstOrDefault(u => u.Username == username);
|
||||
if (userEntity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DbContext.BlackLists.Add(new BlackListEntity
|
||||
{ Email = userEntity.Email, ExpirationDate = DateOnly.FromDateTime(DateTime.Now.AddYears(2)) });
|
||||
DbContext.Users.Remove(userEntity);
|
||||
DbContext.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool UnbanUser(string email)
|
||||
{
|
||||
var blackListEntity = DbContext.BlackLists.FirstOrDefault(b => b.Email == email);
|
||||
if (blackListEntity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DbContext.BlackLists.Remove(blackListEntity);
|
||||
DbContext.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Model.OrderCriteria;
|
||||
|
||||
public enum BlackListOdrerCriteria
|
||||
{
|
||||
None, ByEmail, ByExpirationDate
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
using Model.OrderCriteria;
|
||||
|
||||
namespace Shared;
|
||||
|
||||
public interface IBlackListService<TBlackList>
|
||||
{
|
||||
public IEnumerable<TBlackList> GetBannedUsers(int page, int number, BlackListOdrerCriteria orderCriteria);
|
||||
public int GetNumberOfBannedUsers();
|
||||
public TBlackList? GetUserBannedByEmail(string email);
|
||||
public bool BanUser(string username);
|
||||
public bool UnbanUser(string email);
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
using API.Controllers;
|
||||
using Dto;
|
||||
using Entities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Model.OrderCriteria;
|
||||
using Moq;
|
||||
using Shared;
|
||||
using TestAPI.Extensions;
|
||||
|
||||
namespace TestAPI;
|
||||
|
||||
public class BlackListUnitTest
|
||||
{
|
||||
private readonly Mock<IBlackListService<BlackListDto>> _blackListService;
|
||||
|
||||
public BlackListUnitTest()
|
||||
{
|
||||
_blackListService = new Mock<IBlackListService<BlackListDto>>();
|
||||
}
|
||||
[Fact]
|
||||
public void IsBanned()
|
||||
{
|
||||
_blackListService.Setup(x => x.GetUserBannedByEmail("email@example.com"))
|
||||
.Returns(new BlackListDto { Email = "email@example.com", ExpirationDate = DateOnly.FromDateTime(DateTime.Now)});
|
||||
var usersController = new BlackListController(new NullLogger<UsersController>(), _blackListService.Object);
|
||||
var result = usersController.GetUserBannedByEmail("email@example.com");
|
||||
Assert.Equal(typeof(OkObjectResult), result.GetType());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsBannedNotFound()
|
||||
{
|
||||
_blackListService.Setup(x => x.GetUserBannedByEmail("example@notfound.com"))
|
||||
.Returns<BlackListDto?>(null);
|
||||
var usersController = new BlackListController(new NullLogger<UsersController>(), _blackListService.Object);
|
||||
var result = usersController.GetUserBannedByEmail("example@notfound.com");
|
||||
Assert.Equal(typeof(NotFoundObjectResult), result.GetType());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BanUser()
|
||||
{
|
||||
_blackListService.Setup(x => x.BanUser("Test1"))
|
||||
.Returns(true);
|
||||
var usersController = new BlackListController(new NullLogger<UsersController>(), _blackListService.Object);
|
||||
|
||||
var userResult = usersController.BanUser("Test1");
|
||||
Assert.Equal(typeof(OkResult), userResult.GetType());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BanUserNotFound()
|
||||
{
|
||||
_blackListService.Setup(x => x.BanUser("Test1"))
|
||||
.Returns(true);
|
||||
var usersController = new BlackListController(new NullLogger<UsersController>(), _blackListService.Object);
|
||||
|
||||
var userResult = usersController.BanUser("Test42");
|
||||
Assert.Equal(typeof(NotFoundResult), userResult.GetType());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnbanUser()
|
||||
{
|
||||
_blackListService.Setup(x => x.UnbanUser("example@email.com"))
|
||||
.Returns(true);
|
||||
var usersController = new BlackListController(new NullLogger<UsersController>(), _blackListService.Object);
|
||||
|
||||
var userResult = usersController.UnbanUser("example@email.com");
|
||||
Assert.Equal(typeof(OkResult), userResult.GetType());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnbanUserNotFound()
|
||||
{
|
||||
_blackListService.Setup(x => x.UnbanUser("example@email.com"))
|
||||
.Returns(false);
|
||||
var usersController = new BlackListController(new NullLogger<UsersController>(), _blackListService.Object);
|
||||
|
||||
var userResult = usersController.UnbanUser("example@email.com");
|
||||
Assert.Equal(typeof(NotFoundResult), userResult.GetType());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBannedUsers_NoneOrderCriteria()
|
||||
{
|
||||
_blackListService.Setup(x => x.GetBannedUsers(1,10,BlackListOdrerCriteria.None))
|
||||
.Returns(new List<BlackListDto>()
|
||||
{
|
||||
new BlackListDto { Email = "example1@email.com" , ExpirationDate = DateOnly.FromDateTime(DateTime.Now) },
|
||||
new BlackListDto { Email = "example2@email.com" , ExpirationDate = DateOnly.FromDateTime(DateTime.Now) },
|
||||
new BlackListDto { Email = "example3@email.com" , ExpirationDate = DateOnly.FromDateTime(DateTime.Now) }
|
||||
});
|
||||
var blackListController = new BlackListController(new NullLogger<UsersController>(), _blackListService.Object);
|
||||
|
||||
var result = blackListController.GetUsers(1,10,BlackListOdrerCriteria.None);
|
||||
Assert.Equal(typeof(OkObjectResult), result.GetType());
|
||||
if (result is OkObjectResult okObjectResult)
|
||||
{
|
||||
var valeur = okObjectResult.Value;
|
||||
|
||||
Assert.NotNull(valeur);
|
||||
Assert.Equal(GetBlackList().ToString(), valeur.ToString());
|
||||
Assert.True(GetBlackList().SequenceEqual(valeur as IEnumerable<BlackListDto>, new BlackListDtoEqualityComparer()));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBannedUsers_OrderByEmail()
|
||||
{
|
||||
_blackListService.Setup(x => x.GetBannedUsers(1,10,BlackListOdrerCriteria.ByEmail))
|
||||
.Returns(new List<BlackListDto>()
|
||||
{
|
||||
new BlackListDto { Email = "example1@email.com" , ExpirationDate = DateOnly.FromDateTime(DateTime.Now) },
|
||||
new BlackListDto { Email = "example2@email.com" , ExpirationDate = DateOnly.FromDateTime(DateTime.Now) },
|
||||
new BlackListDto { Email = "example3@email.com" , ExpirationDate = DateOnly.FromDateTime(DateTime.Now) }
|
||||
});
|
||||
var blackListController = new BlackListController(new NullLogger<UsersController>(), _blackListService.Object);
|
||||
|
||||
var result = blackListController.GetUsers(1,10,BlackListOdrerCriteria.ByEmail);
|
||||
Assert.Equal(typeof(OkObjectResult), result.GetType());
|
||||
if (result is OkObjectResult okObjectResult)
|
||||
{
|
||||
var valeur = okObjectResult.Value;
|
||||
|
||||
Assert.NotNull(valeur);
|
||||
Assert.Equal(GetBlackList().ToString(), valeur.ToString());
|
||||
Assert.True(GetBlackList().SequenceEqual(valeur as IEnumerable<BlackListDto>, new BlackListDtoEqualityComparer()));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBannedUsers_OrderedByExpirationDate()
|
||||
{
|
||||
_blackListService.Setup(x => x.GetBannedUsers(1,10,BlackListOdrerCriteria.ByExpirationDate))
|
||||
.Returns(new List<BlackListDto>()
|
||||
{
|
||||
new BlackListDto { Email = "example1@email.com" , ExpirationDate = DateOnly.FromDateTime(DateTime.Now) },
|
||||
new BlackListDto { Email = "example2@email.com" , ExpirationDate = DateOnly.FromDateTime(DateTime.Now) },
|
||||
new BlackListDto { Email = "example3@email.com" , ExpirationDate = DateOnly.FromDateTime(DateTime.Now) }
|
||||
});
|
||||
var blackListController = new BlackListController(new NullLogger<UsersController>(), _blackListService.Object);
|
||||
|
||||
var result = blackListController.GetUsers(1,10,BlackListOdrerCriteria.ByExpirationDate);
|
||||
Assert.Equal(typeof(OkObjectResult), result.GetType());
|
||||
if (result is OkObjectResult okObjectResult)
|
||||
{
|
||||
var valeur = okObjectResult.Value;
|
||||
|
||||
Assert.NotNull(valeur);
|
||||
Assert.Equal(GetBlackList().ToString(), valeur.ToString());
|
||||
Assert.True(GetBlackList().SequenceEqual(valeur as IEnumerable<BlackListDto>, new BlackListDtoEqualityComparer()));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNbBannedUsers()
|
||||
{
|
||||
_blackListService.Setup(x => x.GetNumberOfBannedUsers())
|
||||
.Returns(10);
|
||||
var usersController = new BlackListController(new NullLogger<UsersController>(), _blackListService.Object);
|
||||
|
||||
var userResult = usersController.GetNumberOfBannedUsers();
|
||||
Assert.Equal(typeof(OkObjectResult), userResult.GetType());
|
||||
Assert.Equal(10, (userResult as OkObjectResult).Value);
|
||||
}
|
||||
|
||||
private IEnumerable<BlackListDto> GetBlackList()
|
||||
{
|
||||
return new List<BlackListDto>()
|
||||
{
|
||||
new BlackListDto { Email = "example1@email.com" , ExpirationDate = DateOnly.FromDateTime(DateTime.Now) },
|
||||
new BlackListDto { Email = "example2@email.com" , ExpirationDate = DateOnly.FromDateTime(DateTime.Now) },
|
||||
new BlackListDto { Email = "example3@email.com" , ExpirationDate = DateOnly.FromDateTime(DateTime.Now) }
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
using Dto;
|
||||
|
||||
namespace TestAPI.Extensions;
|
||||
|
||||
public class BlackListDtoEqualityComparer : EqualityComparer<BlackListDto>
|
||||
{
|
||||
public override bool Equals(BlackListDto x, BlackListDto y)
|
||||
{
|
||||
return x.Email == y.Email;
|
||||
}
|
||||
|
||||
public override int GetHashCode(BlackListDto obj)
|
||||
{
|
||||
return obj.Email.GetHashCode();
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
using DbContextLib;
|
||||
using DbDataManager.Service;
|
||||
using Entities;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Model.OrderCriteria;
|
||||
|
||||
namespace TestEF.Service;
|
||||
|
||||
public class TestBlackListDataService
|
||||
{
|
||||
private readonly UserDbContext _dbContext;
|
||||
private readonly BlackListDataService _blackListDataService;
|
||||
|
||||
public TestBlackListDataService()
|
||||
{
|
||||
var connection = new SqliteConnection("DataSource=:memory:");
|
||||
connection.Open();
|
||||
var options = new DbContextOptionsBuilder<UserDbContext>()
|
||||
.UseSqlite(connection)
|
||||
.Options;
|
||||
|
||||
_dbContext = new UserDbContext(options);
|
||||
_blackListDataService = new BlackListDataService(_dbContext);
|
||||
}
|
||||
[Fact]
|
||||
public void BanUser_Success()
|
||||
{
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 1, Username = "Test1", Email = "example@email.com", Password = "password", IsAdmin = true });
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 2, Username = "Test2", Email = "example@email.com", Password = "password", IsAdmin = false });
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 3, Username = "Test3", Email = "example@email.com", Password = "password", IsAdmin = true });
|
||||
_dbContext.SaveChanges();
|
||||
var banResult = _blackListDataService.BanUser("Test1");
|
||||
Assert.True(banResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNbBannedUsers()
|
||||
{
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 1, Username = "Test1", Email = "example1@email.com", Password = "password", IsAdmin = true });
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 2, Username = "Test2", Email = "example2@email.com", Password = "password", IsAdmin = false });
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 3, Username = "Test3", Email = "example3@email.com", Password = "password", IsAdmin = true });
|
||||
_dbContext.SaveChanges();
|
||||
var banResult1 = _blackListDataService.BanUser("Test1");
|
||||
var banResult2 = _blackListDataService.BanUser("Test2");
|
||||
var banResult3 = _blackListDataService.BanUser("Test3");
|
||||
Assert.True(banResult1);
|
||||
Assert.True(banResult2);
|
||||
Assert.True(banResult3);
|
||||
Assert.Equal(3, _dbContext.BlackLists.Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BanUser_Fail()
|
||||
{
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 1, Username = "Test1", Email = "example@email.com", Password = "password", IsAdmin = true });
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 2, Username = "Test2", Email = "example@email.com", Password = "password", IsAdmin = false });
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 3, Username = "Test3", Email = "example@email.com", Password = "password", IsAdmin = true });
|
||||
_dbContext.SaveChanges();
|
||||
var banResult = _blackListDataService.BanUser("Test42");
|
||||
Assert.False(banResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsBanned_Success()
|
||||
{
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 1, Username = "Test1", Email = "example1@email.com", Password = "password", IsAdmin = true });
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 2, Username = "Test2", Email = "example@email.com", Password = "password", IsAdmin = false });
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 3, Username = "Test3", Email = "example@email.com", Password = "password", IsAdmin = true });
|
||||
_dbContext.SaveChanges();
|
||||
var banResult = _blackListDataService.BanUser("Test1");
|
||||
Assert.True(banResult);
|
||||
Assert.NotNull(_blackListDataService.GetUserBannedByEmail("example1@email.com"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnbanUser_Success()
|
||||
{
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 1, Username = "Test1", Email = "example1@email.com", Password = "password", IsAdmin = true });
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 2, Username = "Test2", Email = "example@email.com", Password = "password", IsAdmin = false });
|
||||
_dbContext.Users.Add(new UserEntity() { Id = 3, Username = "Test3", Email = "example@email.com", Password = "password", IsAdmin = true });
|
||||
_dbContext.SaveChanges();
|
||||
var banResult = _blackListDataService.BanUser("Test1");
|
||||
Assert.True(banResult);
|
||||
Assert.True(_blackListDataService.UnbanUser("example1@email.com"));
|
||||
}
|
||||
}
|
Loading…
Reference in new issue