diff --git a/Govor.API.Tests/IntegrationTests/Controllers/ProfileControllerTests.cs b/Govor.API.Tests/IntegrationTests/Controllers/ProfileControllerTests.cs index ac561c9..323d590 100644 --- a/Govor.API.Tests/IntegrationTests/Controllers/ProfileControllerTests.cs +++ b/Govor.API.Tests/IntegrationTests/Controllers/ProfileControllerTests.cs @@ -1,24 +1,35 @@ -using AutoMapper; +using System.Text; +using AutoMapper; using Govor.API.Controllers; +using Govor.API.Hubs; using Govor.Application.Interfaces; using Govor.Application.Interfaces.Infrastructure.Extensions; using Govor.Application.Interfaces.Medias; using Govor.Application.Profiles; using Govor.Contracts.DTOs; +using Govor.Contracts.Requests; +using Govor.Core.Models; +using Govor.Core.Models.Messages; using Govor.Data.Repositories.Exceptions; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using Moq; +using Newtonsoft.Json.Linq; namespace Govor.API.Tests; [TestFixture] public class ProfileControllerTests { - private Mock> _mockLogger = null!; +private Mock> _mockLogger = null!; private Mock _mockMediaService = null!; private Mock _mockProfileService = null!; private Mock _mockCurrentUserService = null!; + private Mock> _mockProfileHubContext = null!; + private Mock _mockClients = null!; + private Mock _mockClientProxy = null!; private Mock _mockMapper = null!; private ProfileController _controller = null!; @@ -32,18 +43,162 @@ public class ProfileControllerTests _mockProfileService = new Mock(); _mockCurrentUserService = new Mock(); _mockMapper = new Mock(); + + _mockProfileHubContext = new Mock>(); + _mockClients = new Mock(); + _mockClientProxy = new Mock(); + + _mockProfileHubContext.Setup(x => x.Clients).Returns(_mockClients.Object); + _mockClients.Setup(x => x.All).Returns(_mockClientProxy.Object); + _mockClientProxy.Setup(x => x.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); _controller = new ProfileController( _mockLogger.Object, _mockMediaService.Object, _mockProfileService.Object, _mockCurrentUserService.Object, + _mockProfileHubContext.Object, _mockMapper.Object); _userId = Guid.NewGuid(); _mockCurrentUserService.Setup(x => x.GetCurrentUserId()).Returns(_userId); } + [Test] + public async Task UploadAvatar_ReturnsBadRequest_WhenFileIsEmpty() + { + // Arrange + var request = new AvatarUploadRequest + { + FromFile = null + }; + + // Act + var result = await _controller.UploadAvatar(request); + + // Assert + Assert.That(result, Is.InstanceOf()); + var badRequestResult = (BadRequestObjectResult)result; + Assert.That(badRequestResult.Value, Is.EqualTo("File is empty.")); + + _mockMediaService.Verify(s => s.UploadMediaAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task UploadAvatar_ReturnsBadRequest_WhenFileLengthIsZero() + { + // Arrange + var mockFile = new Mock(); + mockFile.Setup(f => f.Length).Returns(0); + + var request = new AvatarUploadRequest + { + FromFile = mockFile.Object + }; + + // Act + var result = await _controller.UploadAvatar(request); + + // Assert + Assert.That(result, Is.InstanceOf()); + var badRequestResult = (BadRequestObjectResult)result; + Assert.That(badRequestResult.Value, Is.EqualTo("File is empty.")); + + _mockMediaService.Verify(s => s.UploadMediaAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task UploadAvatar_ReturnsOkAndPerformsAllActions_WhenFileIsValid() + { + // Arrange + var mediaId = Guid.NewGuid(); + var expectedMediaInfo = new MediaUploadResult(mediaId, $"/media/{mediaId}"); + + var fileName = "test_avatar.jpg"; + var fileContent = "This is a test image content"; + var stream = new MemoryStream(Encoding.UTF8.GetBytes(fileContent)); + + var mockFile = new Mock(); + mockFile.Setup(f => f.FileName).Returns(fileName); + mockFile.Setup(f => f.Length).Returns(stream.Length); + mockFile.Setup(f => f.OpenReadStream()).Returns(stream); + mockFile.Setup(f => f.CopyToAsync(It.IsAny(), It.IsAny())) + .Callback((s, t) => stream.CopyTo(s)); // Для ReadFileAsync + + var request = new AvatarUploadRequest + { + FromFile = mockFile.Object, + Type = MediaType.Image, + MimeType = "image/jpeg" + }; + + _mockMediaService.Setup(s => s.UploadMediaAsync(It.IsAny())) + .ReturnsAsync(expectedMediaInfo); + + _mockProfileService.Setup(s => s.SetNewIcon(_userId, mediaId)) + .Returns(Task.CompletedTask); + + // Act + var result = await _controller.UploadAvatar(request); + + // Assert + Assert.That(result, Is.InstanceOf()); + var okResult = (OkObjectResult)result; + Assert.That(okResult.StatusCode, Is.EqualTo(200)); + Assert.That(okResult.Value, Is.EqualTo(expectedMediaInfo)); + + mockFile.Verify(f => f.CopyToAsync(It.IsAny(), It.IsAny()), Times.Once); + + _mockMediaService.Verify(s => s.UploadMediaAsync(It.Is(m => + m.OwnerId == _userId && + m.OwnerType == MediaOwnerType.Avatar && + m.FileName == fileName + )), Times.Once); + + _mockProfileService.Verify(s => s.SetNewIcon(_userId, mediaId), Times.Once); + + _mockClientProxy.Verify( + c => c.SendCoreAsync("AvatarUpdated", It.Is( + args => args.Length == 1 && + JObject.FromObject(args[0]!).Value("userId") == _userId && + JObject.FromObject(args[0]!).Value("iconId") == mediaId + ), It.IsAny()), + Times.Once, + "Hub SendAsync must be called to notify clients." + ); + } + + [Test] + public async Task UploadAvatar_ReturnsInternalServerError_WhenMediaServiceFails() + { + // Arrange + var mockFile = new Mock(); + mockFile.Setup(f => f.Length).Returns(100); + mockFile.Setup(f => f.OpenReadStream()).Returns(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + var request = new AvatarUploadRequest { FromFile = mockFile.Object }; + + _mockMediaService.Setup(s => s.UploadMediaAsync(It.IsAny())) + .ThrowsAsync(new System.Exception("Simulated upload failure")); + + // Act + var result = await _controller.UploadAvatar(request); + + // Assert + Assert.That(result, Is.InstanceOf()); + var objectResult = (ObjectResult)result; + Assert.That(objectResult.StatusCode, Is.EqualTo(500)); + + _mockProfileService.Verify(s => s.SetNewIcon(It.IsAny(), It.IsAny()), Times.Never); + + _mockClientProxy.Verify( + c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never, + "The Hub should not be called in case of a service error." + ); + } + [Test] public async Task DowloadProfile_ReturnsOk_WhenProfileExists() { @@ -71,7 +226,7 @@ public class ProfileControllerTests .Returns(userProfileDto); // Act - var result = await _controller.DowloadProfile(); + var result = await _controller.DownloadProfile(); // Assert var okResult = result as OkObjectResult; @@ -80,22 +235,6 @@ public class ProfileControllerTests Assert.That(okResult.Value, Is.EqualTo(userProfileDto)); } - [Test] - public async Task DowloadProfile_ReturnsNotFound_WhenProfileIsNull() - { - // Arrange - _mockProfileService.Setup(s => s.GetUserProfileAsync(_userId)) - .ReturnsAsync((UserProfile?)null); - - // Act - var result = await _controller.DowloadProfile(); - - // Assert - var notFoundResult = result as NotFoundObjectResult; - Assert.That(notFoundResult, Is.Not.Null); - Assert.That(notFoundResult!.Value, Is.EqualTo("Profile not found")); - } - [Test] public async Task DowloadProfile_ReturnsForbid_WhenUnauthorizedAccessExceptionThrown() { @@ -104,7 +243,7 @@ public class ProfileControllerTests .ThrowsAsync(new UnauthorizedAccessException("Access denied")); // Act - var result = await _controller.DowloadProfile(); + var result = await _controller.DownloadProfile(); // Assert Assert.That(result, Is.InstanceOf()); @@ -115,15 +254,15 @@ public class ProfileControllerTests { // Arrange _mockProfileService.Setup(s => s.GetUserProfileAsync(_userId)) - .ThrowsAsync(new NotFoundException("User not found")); + .ThrowsAsync(new NotFoundException("Cannot find profile")); // Act - var result = await _controller.DowloadProfile(); + var result = await _controller.DownloadProfile(); // Assert - var badRequest = result as BadRequestObjectResult; - Assert.That(badRequest, Is.Not.Null); - Assert.That(badRequest!.Value, Is.EqualTo("User not found")); + var notFoundResult = result as NotFoundObjectResult; + Assert.That(notFoundResult, Is.Not.Null); + Assert.That(notFoundResult!.Value, Is.EqualTo("Profile not found")); } [Test] @@ -134,7 +273,7 @@ public class ProfileControllerTests .ThrowsAsync(new Exception("Unexpected error")); // Act - var result = await _controller.DowloadProfile(); + var result = await _controller.DownloadProfile(); // Assert var objectResult = result as ObjectResult; diff --git a/Govor.API/Controllers/Authentication/AuthController.cs b/Govor.API/Controllers/Authentication/AuthController.cs index 7446985..897803f 100644 --- a/Govor.API/Controllers/Authentication/AuthController.cs +++ b/Govor.API/Controllers/Authentication/AuthController.cs @@ -30,7 +30,7 @@ public class AuthController : Controller _logger = logger; } - [RequireHttps] + //[RequireHttps] [HttpPost("register")]// api/auth/register public async Task Register([FromBody] RegistrationRequest registrationRequest) { @@ -74,7 +74,7 @@ public class AuthController : Controller } } - [RequireHttps] + //[RequireHttps] [HttpPost("login")]// api/auth/login public async Task Login([FromBody] LoginRequest loginRequest) { diff --git a/Govor.API/Controllers/Authentication/RefreshController.cs b/Govor.API/Controllers/Authentication/RefreshController.cs index 426d4bb..ebf95c1 100644 --- a/Govor.API/Controllers/Authentication/RefreshController.cs +++ b/Govor.API/Controllers/Authentication/RefreshController.cs @@ -14,13 +14,15 @@ public class RefreshController : Controller private readonly ILogger _logger; private readonly IUserSessionRefresher _userSession; - public RefreshController(ILogger logger, IUserSessionRefresher userSession) + public RefreshController( + ILogger logger, + IUserSessionRefresher userSession) { _logger = logger; _userSession = userSession; } - [RequireHttps] + //[RequireHttps] [HttpPost("refresh")] // api/auth/token/refresh public async Task Refresh([FromBody] RefreshTokenRequest refreshRequest) { diff --git a/Govor.API/Controllers/Friends/FriendsRequestQueryController.cs b/Govor.API/Controllers/Friends/FriendsRequestQueryController.cs index 9609238..542ee89 100644 --- a/Govor.API/Controllers/Friends/FriendsRequestQueryController.cs +++ b/Govor.API/Controllers/Friends/FriendsRequestQueryController.cs @@ -8,9 +8,10 @@ using Microsoft.AspNetCore.Mvc; namespace Govor.API.Controllers.Friends; -[ApiController] + [Authorize] [Route("api/friends")] +[ApiController] public class FriendsRequestQueryController : Controller { private readonly ILogger _logger; @@ -18,7 +19,8 @@ public class FriendsRequestQueryController : Controller private readonly ICurrentUserService _currentUserService; private readonly IMapper _mapper; - public FriendsRequestQueryController(ILogger logger, + public FriendsRequestQueryController( + ILogger logger, IFriendRequestQueryService friendsService, ICurrentUserService currentUserService, IMapper mapper) diff --git a/Govor.API/Controllers/Friends/FriendshipController.cs b/Govor.API/Controllers/Friends/FriendshipController.cs index 61bb81a..25c0065 100644 --- a/Govor.API/Controllers/Friends/FriendshipController.cs +++ b/Govor.API/Controllers/Friends/FriendshipController.cs @@ -16,7 +16,8 @@ public class FriendshipController : Controller private readonly ICurrentUserService _currentUserService; private readonly IMapper _mapper; - public FriendshipController(ILogger logger, + public FriendshipController( + ILogger logger, IFriendshipService friendsService, ICurrentUserService currentUserService, IMapper mapper) diff --git a/Govor.API/Controllers/ProfileController.cs b/Govor.API/Controllers/ProfileController.cs index 8e3eaee..fe2d327 100644 --- a/Govor.API/Controllers/ProfileController.cs +++ b/Govor.API/Controllers/ProfileController.cs @@ -1,11 +1,15 @@ using AutoMapper; +using Govor.API.Hubs; using Govor.Application.Interfaces; using Govor.Application.Interfaces.Infrastructure.Extensions; using Govor.Application.Interfaces.Medias; using Govor.Contracts.DTOs; +using Govor.Contracts.Requests; +using Govor.Core.Models; using Govor.Data.Repositories.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; namespace Govor.API.Controllers; @@ -18,6 +22,7 @@ public class ProfileController : ControllerBase private readonly IMediaService _mediaService; private readonly IProfileService _profileService; private readonly ICurrentUserService _currentUserService; + private readonly IHubContext _profileHubContext; private readonly IMapper _mapper; public ProfileController( @@ -25,26 +30,80 @@ public class ProfileController : ControllerBase IMediaService mediaService, IProfileService profileService, ICurrentUserService currentUserService, + IHubContext profileHubContext, IMapper mapper) { _logger = logger; _mediaService = mediaService; _profileService = profileService; + _profileHubContext = profileHubContext; _currentUserService = currentUserService; _mapper = mapper; } + + [RequestSizeLimit(40_000)] + [HttpPost("avatar")] // api/profile/avatar + public async Task UploadAvatar([FromForm] AvatarUploadRequest request) + { + + var userId = _currentUserService.GetCurrentUserId(); + + if (request.FromFile == null || request.FromFile.Length == 0) + { + return BadRequest("File is empty."); + } + + try + { + byte[] fileBytes = await ReadFileAsync(request.FromFile); - [HttpGet("dowload")] - public async Task DowloadProfile() + var media = new Media( + _currentUserService.GetCurrentUserId(), + DateTime.UtcNow, + Path.GetFileName(request.FromFile.FileName), + fileBytes, + request.Type, + request.MimeType, + String.Empty, + MediaOwnerType.Avatar, + userId); + + var mediaInfo = await _mediaService.UploadMediaAsync(media); + + await _profileService.SetNewIcon(userId, mediaInfo.MediaId); + var iconId = mediaInfo.MediaId; + + var payload = new { userId, iconId }; + + await _profileHubContext.Clients.All.SendAsync( + "AvatarUpdated", + payload + ); + + + return Ok(mediaInfo); + } + catch (System.Exception ex) + { + return StatusCode(500, new { Message = "Avatar processing error.", Details = ex.Message }); + } + } + + private async Task ReadFileAsync(IFormFile file) + { + await using var ms = new MemoryStream(); + await file.CopyToAsync(ms); + return ms.ToArray(); + } + + [HttpGet("dowload/me")] + public async Task DownloadProfile() { try { var userId = _currentUserService.GetCurrentUserId(); var user = await _profileService.GetUserProfileAsync(userId); - - if (user is null) - return NotFound("Profile not found"); - + var dto = _mapper.Map(user); return Ok(dto); } @@ -56,7 +115,34 @@ public class ProfileController : ControllerBase catch (NotFoundException ex) { _logger.LogWarning(ex, ex.Message); - return BadRequest(ex.Message); + return NotFound("Profile not found"); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(500, "Unexpected Error! Please try again later."); + } + } + + [HttpGet("download/{id}")] + public async Task DowloadProfileByUserId(Guid id) + { + try + { + var user = await _profileService.GetUserProfileAsync(id); + + var dto = _mapper.Map(user); + return Ok(dto); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogWarning(ex.Message); + return Forbid(ex.Message); + } + catch (NotFoundException ex) + { + _logger.LogWarning(ex, ex.Message); + return NotFound("Profile not found"); } catch (Exception ex) { diff --git a/Govor.API/Hubs/ChatsHub.cs b/Govor.API/Hubs/ChatsHub.cs index 07f64be..9ffedfa 100644 --- a/Govor.API/Hubs/ChatsHub.cs +++ b/Govor.API/Hubs/ChatsHub.cs @@ -19,7 +19,11 @@ public class ChatsHub : Hub private readonly IUserGroupsService _userService; private readonly IHubUserAccessor _userAccessor; - public ChatsHub(ILogger logger, IMessageCommandService messageCommandService, IUserGroupsService userService, IHubUserAccessor userAccessor) + public ChatsHub( + ILogger logger, + IMessageCommandService messageCommandService, + IUserGroupsService userService, + IHubUserAccessor userAccessor) { _logger = logger; _messageCommandService = messageCommandService; @@ -60,10 +64,8 @@ public class ChatsHub : Hub _logger.LogInformation("User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group", userId, Context.ConnectionId); - var userGroups = - await _userService - .GetUserGroupsAsync( - userId); + var userGroups = await _userService.GetUserGroupsAsync(userId); + foreach (var group in userGroups) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"group_{group.Id}"); @@ -236,6 +238,7 @@ public class ChatsHub : Hub } } + #region common private UserMessageResponse BuildUserMessageResponse(Message message, Guid? replyToId) { diff --git a/Govor.API/Hubs/FriendsHub.cs b/Govor.API/Hubs/FriendsHub.cs index 94fa19a..c9a379c 100644 --- a/Govor.API/Hubs/FriendsHub.cs +++ b/Govor.API/Hubs/FriendsHub.cs @@ -111,9 +111,13 @@ public class FriendsHub : Hub { var userId = _userAccessor.GetUserId(Context); var friendship = await _friendRequestService.AcceptAsync(friendshipId, userId); - await Clients.Group(userId.ToString()) - .SendAsync("FriendRequestAccepted", _mapper.Map(friendship)); + await Clients.Group(userId.ToString()) + .SendAsync("FriendRequestAccepted", _mapper.Map(friendship)); + + await Clients.Group(friendship.RequesterId.ToString()) + .SendAsync( "YourFriendRequestAccepted", _mapper.Map(friendship.Addressee)); + _logger.LogInformation($"Friend request accepted for user {userId} from {userId}."); return HubResult.Ok(); } @@ -140,9 +144,14 @@ public class FriendsHub : Hub { var userId = _userAccessor.GetUserId(Context); var friendship = await _friendRequestService.RejectAsync(friendshipId, userId); + var dto = _mapper.Map(friendship); + await Clients.Group(userId.ToString()) - .SendAsync("FriendRequestRejected", _mapper.Map(friendship)); - + .SendAsync("FriendRequestRejected", dto); + + await Clients.Group(friendship.RequesterId.ToString()) + .SendAsync("YourFriendRequestRejected", dto); + _logger.LogInformation($"Friend request rejected for user {userId} from {userId}."); return HubResult.Ok(); } diff --git a/Govor.API/Hubs/ProfileHub.cs b/Govor.API/Hubs/ProfileHub.cs index 6728879..cd74f69 100644 --- a/Govor.API/Hubs/ProfileHub.cs +++ b/Govor.API/Hubs/ProfileHub.cs @@ -1,6 +1,11 @@ using Govor.API.Common.SignalR.Helpers; using Govor.Application.Interfaces; +using Govor.Application.Interfaces.Friends; +using Govor.Application.Interfaces.Infrastructure.Extensions; +using Govor.Application.Interfaces.Medias; using Govor.Contracts.Responses.SignalR; +using Govor.Core.Models; +using Govor.Core.Models.Users; using Govor.Core.Repositories.Groups; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; @@ -11,14 +16,15 @@ namespace Govor.API.Hubs; public class ProfileHub : Hub { private readonly IGroupsRepository _groupsRepository; - private readonly IFriendsService _friendsService; + private readonly IFriendshipService _friendsService; private readonly IProfileService _profileService; private readonly IHubUserAccessor _userAccessor; private readonly ILogger _logger; - + private readonly IMediaService _mediaService; + public ProfileHub( IGroupsRepository groupsRepository, - IFriendsService friendsService, + IFriendshipService friendsService, IProfileService profileService, IHubUserAccessor userAccessor, ILogger logger) @@ -33,19 +39,32 @@ public class ProfileHub : Hub public override async Task OnConnectedAsync() { var userId = _userAccessor.GetUserId(Context); - - // Добавляем в персональную группу + await Groups.AddToGroupAsync(Context.ConnectionId, $"user-{userId}"); // Friends - var friendships = await _friendsService.GetFriendsAsync(userId); - foreach (var friends in friendships) - await Groups.AddToGroupAsync(Context.ConnectionId, $"friends-{friends.Id}"); - + try + { + var friendships = await _friendsService.GetFriendsAsync(userId); + foreach (var friends in friendships) + await Groups.AddToGroupAsync(Context.ConnectionId, $"friends-{friends.Id}"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to get friends for user {userId}", userId); + } + // Groups - var groups = await _groupsRepository.GetByUserIdAsync(userId); - foreach (var group in groups) - await Groups.AddToGroupAsync(Context.ConnectionId, $"group-{group.Id}"); + try + { + var groups = await _groupsRepository.GetByUserIdAsync(userId); + foreach (var group in groups) + await Groups.AddToGroupAsync(Context.ConnectionId, $"group-{group.Id}"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to get groups for user {userId}", userId); + } _logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId}", userId, Context.ConnectionId); @@ -55,24 +74,43 @@ public class ProfileHub : Hub public override async Task OnDisconnectedAsync(Exception exception) { var userId = _userAccessor.GetUserId(Context); - + await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user-{userId}"); + // Friends - var friendships = await _friendsService.GetFriendsAsync(userId); - foreach (var friendship in friendships) - await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"friends-{friendship.Id}"); + try + { + var friendships = await _friendsService.GetFriendsAsync(userId); + + foreach (var friendship in friendships) + await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"friends-{friendship.Id}"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to remove user {userId} from friend groups", userId); + } // Groups - var groups = await _groupsRepository.GetByUserIdAsync(userId); - foreach (var group in groups) - await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"group-{group.Id}"); + try + { + var groups = await _groupsRepository.GetByUserIdAsync(userId); + + foreach (var group in groups) + await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"group-{group.Id}"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to remove user {userId} from groups", userId); + } await base.OnDisconnectedAsync(exception); } - - + public async Task> SetDescription(string description) { + if(description.Length > 500) + return HubResult.Error("The description cannot be longer than 500 characters."); + var userId = _userAccessor.GetUserId(Context); try @@ -115,26 +153,51 @@ public class ProfileHub : Hub return HubResult.Error("An unaccounted error on the server!"); } } - + private async Task NotifyFriendsAndGroupsAsync(Guid userId, string eventName, object payload) { - var friendIds = await _friendsService.GetFriendsAsync(userId); - var groups = await _groupsRepository.GetByUserIdAsync(userId); + var friendIds = Enumerable.Empty(); + var groups = Enumerable.Empty(); + + try + { + friendIds = await _friendsService.GetFriendsAsync(userId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to load friend list for notifications. userId: {userId}", userId); + } + + try + { + groups = await _groupsRepository.GetByUserIdAsync(userId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to load group list for notifications. userId: {userId}", userId); + } var groupIds = groups.Select(g => g.Id).ToList(); - await Clients.Group($"user-{userId}") // current user + // Current user + await Clients.Group($"user-{userId}") .SendAsync(eventName, payload); - foreach (var friendId in friendIds) - await Clients.Group($"friends-{friendId}") + // Friends + foreach (var friend in friendIds) + { + await Clients.Group($"friends-{friend.Id}") .SendAsync(eventName, payload); + } + // Groups foreach (var groupId in groupIds) + { await Clients.Group($"group-{groupId}") .SendAsync(eventName, payload); + } _logger.LogInformation("Sent {EventName} for {UserId} to {Friends} friends and {Groups} groups", - eventName, userId, friendIds.Count, groupIds.Count); + eventName, userId, friendIds.Count(), groupIds.Count); } } diff --git a/Govor.API/Program.cs b/Govor.API/Program.cs index a8f3785..6489eb3 100644 --- a/Govor.API/Program.cs +++ b/Govor.API/Program.cs @@ -133,7 +133,8 @@ app.MapHub("/hubs/chats"); app.MapHub("/hubs/friends"); app.MapHub("/hubs/profiles"); -app.MapSwagger().RequireAuthorization(); +app.MapSwagger() + .RequireAuthorization(); app.Map("/", () => "Not for browsers"); diff --git a/Govor.API/Properties/launchSettings.json b/Govor.API/Properties/launchSettings.json index de57ac8..defffb2 100644 --- a/Govor.API/Properties/launchSettings.json +++ b/Govor.API/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://0.0.0.0:8080;http://localhost:5041", + "applicationUrl": "http://0.0.0.0:8080;", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -13,8 +13,8 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "https://0.0.0.0:8080;https://localhost:7155;http://localhost:5041", + "launchBrowser": true, + "applicationUrl": "https://0.0.0.0:8080;", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Govor.API/appsettings.json b/Govor.API/appsettings.json index f546cee..d3b602d 100644 --- a/Govor.API/appsettings.json +++ b/Govor.API/appsettings.json @@ -6,7 +6,7 @@ } }, "ConnectionStrings": { - "GovorDbContext": "Host=77.233.213.226;Port=5432;Database=GovorDb;Username=gen_user;Password=co|HPq}JI6D@,t;" + "GovorDbContext": "Host=localhost;Port=5432;Database=GovorDb;Username=postgres;Password=stalcker;" }, "UseMySql": false, "AllowedHosts": "*", diff --git a/Govor.Application.Tests/Services/Authentication/AuthServiceTests.cs b/Govor.Application.Tests/Services/Authentication/AuthServiceTests.cs index 4a85b28..ee3964a 100644 --- a/Govor.Application.Tests/Services/Authentication/AuthServiceTests.cs +++ b/Govor.Application.Tests/Services/Authentication/AuthServiceTests.cs @@ -43,7 +43,6 @@ public class AuthServiceTests _accountService = new AuthService( _usersRepositoryMock.Object, - _jwtServiceMock.Object, _passwordHasherMock.Object, _adminsRepositoryMock.Object, _usernameValidatorMock.Object diff --git a/Govor.Application.Tests/Services/Friends/FriendshipServiceTests.cs b/Govor.Application.Tests/Services/Friends/FriendshipServiceTests.cs index 672dc86..acd7d08 100644 --- a/Govor.Application.Tests/Services/Friends/FriendshipServiceTests.cs +++ b/Govor.Application.Tests/Services/Friends/FriendshipServiceTests.cs @@ -1,5 +1,4 @@ using AutoFixture; -using Govor.Application.Exceptions.FriendsService; using Govor.Application.Interfaces.Friends; using Govor.Application.Services.Friends; using Govor.Core.Models; @@ -115,12 +114,14 @@ public class FriendshipServiceTests public void SearchUsersAsync_Throws_UnauthorizedAccessException_IfThrowsSomeExceptionInFriendshipsRepository() { // Arrange - _friendshipsRepositoryMock - .Setup(u => u.FindByUserIdAsync(It.IsAny())) + _usersRepositoryMock + .Setup(u => u.SearchPotentialFriendsAsync(It.IsAny(), "test")) .ThrowsAsync(new Exception("test")); // Act & Assert - Assert.ThrowsAsync(async () => await _service.SearchUsersAsync("test", Guid.NewGuid())); + Assert.ThrowsAsync( + async () => await _service.SearchUsersAsync("test", Guid.NewGuid()) + ); } // GetFriendsAsync diff --git a/Govor.Application.Tests/Services/Medias/AccesserToDownloadMediaServiceTests.cs b/Govor.Application.Tests/Services/Medias/AccesserToDownloadMediaServiceTests.cs index 990ec2a..8c45613 100644 --- a/Govor.Application.Tests/Services/Medias/AccesserToDownloadMediaServiceTests.cs +++ b/Govor.Application.Tests/Services/Medias/AccesserToDownloadMediaServiceTests.cs @@ -31,7 +31,7 @@ public class AccesserToDownloadMediaServiceTests _groupId = Guid.NewGuid(); _mediaFileId = Guid.NewGuid(); - // + // ������� ��������� � ����� var message = new Message { Id = Guid.NewGuid(), @@ -170,7 +170,7 @@ public class AccesserToDownloadMediaServiceTests } [Test] - public async Task HasAccessAsync_ReturnsFalse_ForOtherUserAvatar() + public async Task HasAccessAsync_ReturnsTrue_ForOtherUserAvatar() { var avatarMedia = new MediaFile { @@ -188,7 +188,7 @@ public class AccesserToDownloadMediaServiceTests await _dbContext.SaveChangesAsync(); var result = await _accesser.HasAccessAsync(avatarMedia.Id, _userId); - Assert.That(result, Is.False); + Assert.That(result, Is.True); } [Test] @@ -275,7 +275,7 @@ public class AccesserToDownloadMediaServiceTests MineType = "image/png", MediaType = MediaType.Image, UploaderId = _userId, - OwnerType = (MediaOwnerType)999, // + OwnerType = (MediaOwnerType)999, // ����������� ��� DateCreated = DateTime.UtcNow }; diff --git a/Govor.Application.Tests/Services/ProfileServiceTests.cs b/Govor.Application.Tests/Services/ProfileServiceTests.cs index fa2dfb8..45e14a6 100644 --- a/Govor.Application.Tests/Services/ProfileServiceTests.cs +++ b/Govor.Application.Tests/Services/ProfileServiceTests.cs @@ -1,6 +1,7 @@ using Govor.Application.Services; using Govor.Core.Models.Users; using Govor.Core.Repositories.Users; +using Govor.Data.Repositories.Exceptions; using Microsoft.Extensions.Logging; using Moq; @@ -64,7 +65,19 @@ public class ProfileServiceTests Assert.ThrowsAsync(() => _service.GetUserProfileAsync(Guid.NewGuid())); } + + [Test] + public void GetUserProfileAsync_ThrowsNotFoundException_WhenNotFoundByKeyException() + { + // Arrange + var userId = Guid.NewGuid(); + _mockUsersRepo.Setup(r => r.FindByIdAsync(It.IsAny())) + .ThrowsAsync(new NotFoundByKeyException(userId)); + // Act + Assert + Assert.ThrowsAsync(() => + _service.GetUserProfileAsync(userId)); + } // ---------- SetDescription ---------- [Test] diff --git a/Govor.Application/Interfaces/Medias/IMediaService.cs b/Govor.Application/Interfaces/Medias/IMediaService.cs index 329e4d3..18769cd 100644 --- a/Govor.Application/Interfaces/Medias/IMediaService.cs +++ b/Govor.Application/Interfaces/Medias/IMediaService.cs @@ -22,4 +22,4 @@ public record Media(Guid UploaderId, MediaOwnerType OwnerType, Guid? OwnerId); -public record MediaUploadResult(Guid? MediaId, string Url); \ No newline at end of file +public record MediaUploadResult(Guid MediaId, string Url); \ No newline at end of file diff --git a/Govor.Application/Services/Authentication/AuthService.cs b/Govor.Application/Services/Authentication/AuthService.cs index fd60799..0b46164 100644 --- a/Govor.Application/Services/Authentication/AuthService.cs +++ b/Govor.Application/Services/Authentication/AuthService.cs @@ -11,20 +11,17 @@ namespace Govor.Application.Services.Authentication; public class AuthService : IAccountService { private readonly IPasswordHasher _passwordHasher; - private readonly IJwtService _jwtService; private readonly IUsersRepository _usersRepository; private readonly IAdminsRepository _adminsRepository; private readonly IUsernameValidator _usernameValidator; public AuthService(IUsersRepository usersRepository, - IJwtService jwtService, IPasswordHasher passwordHasher, IAdminsRepository adminsRepository, IUsernameValidator usernameValidator ) { _usersRepository = usersRepository; - _jwtService = jwtService; _passwordHasher = passwordHasher; _adminsRepository = adminsRepository; _usernameValidator = usernameValidator; diff --git a/Govor.Application/Services/Friends/FriendshipService.cs b/Govor.Application/Services/Friends/FriendshipService.cs index 20e3a4a..9944e68 100644 --- a/Govor.Application/Services/Friends/FriendshipService.cs +++ b/Govor.Application/Services/Friends/FriendshipService.cs @@ -27,9 +27,7 @@ public class FriendshipService : IFriendshipService { all = await _usersRepository.SearchPotentialFriendsAsync(currentId, query); - return all - .Where(u => u.Id != currentId) - .ToList(); + return all; } catch (NotFoundByKeyException<(string, Guid)> ex) { @@ -37,7 +35,7 @@ public class FriendshipService : IFriendshipService } catch (NotFoundByKeyException ex) { - return all.Where(u => u.Id != currentId).ToList(); + return []; } catch (Exception ex) { @@ -58,7 +56,7 @@ public class FriendshipService : IFriendshipService } catch (NotFoundByKeyException ex) { - throw new InvalidOperationException("User not found", ex); + throw new InvalidOperationException("Nothing was found for the specified id.", ex); } } } \ No newline at end of file diff --git a/Govor.Application/Services/InvitesService.cs b/Govor.Application/Services/InvitesService.cs index a9ea40e..f281025 100644 --- a/Govor.Application/Services/InvitesService.cs +++ b/Govor.Application/Services/InvitesService.cs @@ -49,11 +49,5 @@ public class InvitesService : IInvitesService throw new InviteLinkInvalidException(inviteCode); } } - - - public string GenerateInvitationLink(Invitation invitation) - { - throw new NotImplementedException(); - } } diff --git a/Govor.Application/Services/ProfileService.cs b/Govor.Application/Services/ProfileService.cs index 2d97aa8..0ec2073 100644 --- a/Govor.Application/Services/ProfileService.cs +++ b/Govor.Application/Services/ProfileService.cs @@ -1,6 +1,7 @@ using Govor.Application.Interfaces; using Govor.Application.Profiles; using Govor.Core.Repositories.Users; +using Govor.Data.Repositories.Exceptions; using Microsoft.Extensions.Logging; namespace Govor.Application.Services; @@ -19,15 +20,23 @@ public class ProfileService : IProfileService public async Task GetUserProfileAsync(Guid userId) { _logger.LogInformation("Gettings user {userId} profile", userId); - var user = await _userRepository.FindByIdAsync(userId); - - return new UserProfile + try { - Id = user.Id, - Username = user.Username, - Description = user.Description, - IconId = user.IconId - }; + var user = await _userRepository.FindByIdAsync(userId); + + return new UserProfile + { + Id = user.Id, + Username = user.Username, + Description = user.Description, + IconId = user.IconId + }; + } + catch (NotFoundByKeyException ex) + { + throw new NotFoundException("User's profile cant be found with given id", ex); + } + } public async Task SetDescription(string description, Guid userId) diff --git a/Govor.Contracts/Requests/AvatarUploadRequest.cs b/Govor.Contracts/Requests/AvatarUploadRequest.cs new file mode 100644 index 0000000..3ae6553 --- /dev/null +++ b/Govor.Contracts/Requests/AvatarUploadRequest.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using Govor.Core.Models; +using Govor.Core.Models.Messages; +using Microsoft.AspNetCore.Http; + +namespace Govor.Contracts.Requests; + +public class AvatarUploadRequest +{ + [Required] + public IFormFile FromFile { get; set; } + [Required] + public MediaType Type { get; set; } + [Required, MaxLength(255)] + public string MimeType { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Govor.Data/Configurations/UserConfiguration.cs b/Govor.Data/Configurations/UserConfiguration.cs index fb3bfed..76edac4 100644 --- a/Govor.Data/Configurations/UserConfiguration.cs +++ b/Govor.Data/Configurations/UserConfiguration.cs @@ -13,5 +13,19 @@ public class UserConfiguration : IEntityTypeConfiguration builder.HasOne(u => u.Invite) .WithMany(i => i.Users) .HasForeignKey(u => u.InviteId); + + builder.Property(u => u.Username) + .IsRequired() + .HasMaxLength(50); + + builder.HasIndex(u => u.Username).IsUnique(); + + builder.Property(u => u.Description) + .HasMaxLength(500) + .IsRequired(false); + + builder.Property(u => u.PasswordHash) + .IsRequired() + .HasMaxLength(128); } } \ No newline at end of file