diff --git a/Govor.API.Tests/IntegrationTests/Controllers/MediaControllerTests.cs b/Govor.API.Tests/IntegrationTests/Controllers/MediaControllerTests.cs index 9f80ed2..ad5b5dc 100644 --- a/Govor.API.Tests/IntegrationTests/Controllers/MediaControllerTests.cs +++ b/Govor.API.Tests/IntegrationTests/Controllers/MediaControllerTests.cs @@ -1,3 +1,4 @@ +using System.Text; using AutoFixture; using Govor.API.Controllers; using Govor.Application.Interfaces.Infrastructure.Extensions; @@ -43,7 +44,7 @@ public class MediaControllerTests _currentUserMock.Object); } - // Test for Upload action + // Tests for Upload action [Test] public async Task Upload_ValidRequest_ReturnsOkResult() { @@ -86,6 +87,272 @@ public class MediaControllerTests Assert.That(value.MediaId, Is.EqualTo(uploadResult.MediaId)); } + [Test] + public async Task Upload_InvalidModelState_ReturnsBadRequest() + { + // Arrange + _controller.ModelState.AddModelError("Error", "Invalid model state"); + var content = "fake file content"; + var fileName = "testfile.txt"; + var fileBytes = System.Text.Encoding.UTF8.GetBytes(content); + var stream = new MemoryStream(fileBytes); + + var formFileMock = new Mock(); + formFileMock.Setup(f => f.Length).Returns(fileBytes.Length); + formFileMock.Setup(f => f.FileName).Returns(fileName); + formFileMock.Setup(f => f.OpenReadStream()).Returns(stream); + formFileMock.Setup(f => f.CopyToAsync(It.IsAny(), default)) + .Returns((target, _) => stream.CopyToAsync(target)); + + var uploadRequest = _fixture.Build() + .With(r => r.FromFile, formFileMock.Object) + .With(r => r.Type, MediaType.Image) + .With(r => r.MimeType, "image/png") + .With(r => r.EncryptedKey, "secret") + .Create(); + + // Act + var result = await _controller.Upload(uploadRequest); + + // Assert + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public async Task Upload_NoFileUploaded_ReturnsBadRequest() + { + // Arrange + var uploadRequest = _fixture.Build() + .With(r => r.FromFile, (IFormFile)null) + .Create(); + + // Act + var result = await _controller.Upload(uploadRequest); + + // Assert + Assert.That(result, Is.InstanceOf()); + var badRequestResult = result as BadRequestObjectResult; + Assert.That(badRequestResult?.Value, Is.EqualTo("No file uploaded")); + } + + [Test] + public async Task Upload_EmptyFile_ReturnsBadRequest() + { + // Arrange + var formFileMock = new Mock(); + formFileMock.Setup(f => f.Length).Returns(0); + + var uploadRequest = _fixture.Build() + .With(r => r.FromFile, formFileMock.Object) + .Create(); + + // Act + var result = await _controller.Upload(uploadRequest); + + // Assert + Assert.That(result, Is.InstanceOf()); + var badRequestResult = result as BadRequestObjectResult; + Assert.That(badRequestResult?.Value, Is.EqualTo("No file uploaded")); + } + + [Test] + public async Task Upload_FileTooLarge_ReturnsBadRequest() + { + // Arrange + var formFileMock = new Mock(); + formFileMock.Setup(f => f.Length).Returns(20_000_001); // Just over 20MB + + var uploadRequest = _fixture.Build() + .With(r => r.FromFile, formFileMock.Object) + .Create(); + + // Act + var result = await _controller.Upload(uploadRequest); + + // Assert + Assert.That(result, Is.InstanceOf()); + var badRequestResult = result as BadRequestObjectResult; + Assert.That(badRequestResult?.Value, Is.EqualTo("File is too large")); + } + + [Test] + public async Task Upload_MissingMimeType_ReturnsBadRequest() + { + // Arrange + var formFileMock = new Mock(); + formFileMock.Setup(f => f.Length).Returns(1000); + formFileMock.Setup(f => f.FileName).Returns("testfile.txt"); + + var uploadRequest = _fixture.Build() + .With(r => r.FromFile, formFileMock.Object) + .With(r => r.MimeType, string.Empty) + .Create(); + + // Act + var result = await _controller.Upload(uploadRequest); + + // Assert + Assert.That(result, Is.InstanceOf()); + var badRequestResult = result as BadRequestObjectResult; + Assert.That(badRequestResult?.Value, Is.EqualTo("Missing MIME type")); + } + + [Test] + public async Task Upload_UnauthorizedAccess_ReturnsUnauthorized() + { + // Arrange + var formFileMock = new Mock(); + formFileMock.Setup(f => f.Length).Returns(1000); + formFileMock.Setup(f => f.FileName).Returns("testfile.txt"); + formFileMock.Setup(f => f.OpenReadStream()).Returns(new MemoryStream(new byte[1000])); + + var uploadRequest = _fixture.Build() + .With(r => r.FromFile, formFileMock.Object) + .With(r => r.MimeType, "text/plain") + .Create(); + + _mockMedia.Setup(f => f.UploadMediaAsync(It.IsAny())) + .ThrowsAsync(new UnauthorizedAccessException("Access denied")); + + // Act + var result = await _controller.Upload(uploadRequest); + + // Assert + Assert.That(result, Is.InstanceOf()); + var unauthorizedResult = result as UnauthorizedObjectResult; + Assert.That(unauthorizedResult?.Value, Is.EqualTo("Access denied")); + } + + [Test] + public async Task Upload_InvalidOperation_ReturnsBadRequest() + { + // Arrange + var formFileMock = new Mock(); + formFileMock.Setup(f => f.Length).Returns(1000); + formFileMock.Setup(f => f.FileName).Returns("testfile.txt"); + formFileMock.Setup(f => f.OpenReadStream()).Returns(new MemoryStream(new byte[1000])); + + var uploadRequest = _fixture.Build() + .With(r => r.FromFile, formFileMock.Object) + .With(r => r.MimeType, "text/plain") + .Create(); + + _mockMedia.Setup(f => f.UploadMediaAsync(It.IsAny())) + .ThrowsAsync(new InvalidOperationException("Invalid operation")); + + // Act + var result = await _controller.Upload(uploadRequest); + + // Assert + Assert.That(result, Is.InstanceOf()); + var badRequestResult = result as BadRequestObjectResult; + Assert.That(badRequestResult?.Value, Is.EqualTo("Invalid operation")); + } + + [Test] + public async Task Upload_GeneralException_ReturnsInternalServerError() + { + // Arrange + var formFileMock = new Mock(); + formFileMock.Setup(f => f.Length).Returns(1000); + formFileMock.Setup(f => f.FileName).Returns("testfile.txt"); + formFileMock.Setup(f => f.OpenReadStream()).Returns(new MemoryStream(new byte[1000])); + + var uploadRequest = _fixture.Build() + .With(r => r.FromFile, formFileMock.Object) + .With(r => r.MimeType, "text/plain") + .Create(); + + _mockMedia.Setup(f => f.UploadMediaAsync(It.IsAny())) + .ThrowsAsync(new Exception("Something went wrong")); + + // Act + var result = await _controller.Upload(uploadRequest); + + // Assert + Assert.That(result, Is.InstanceOf()); + var objectResult = result as ObjectResult; + Assert.That(objectResult.StatusCode, Is.EqualTo(500)); + } + + // Tests for Download action + [Test] + public async Task Download_HasAccessAndMediaExists_ReturnsFile() + { + // Arrange + var mediaId = Guid.NewGuid(); + var media = _fixture.Build() + .With(m => m.Data, Encoding.UTF8.GetBytes("fake file content")) + .With(m => m.MimeType, "application/octet-stream") // Ensure MimeType is set + .With(m => m.FileName, "testfile.txt") + .Create(); + + _mockAccesser.Setup(f => f.HasAccessAsync(mediaId, _userId)).ReturnsAsync(true); + _mockMedia.Setup(f => f.GetMediaByIdAsync(mediaId)).ReturnsAsync(media); + + // Act + var result = await _controller.Download(mediaId); + + // Assert + Assert.That(result, Is.InstanceOf()); + var fileResult = result as FileContentResult; + Assert.That(fileResult?.FileContents, Is.EqualTo(media.Data)); + Assert.That(fileResult?.ContentType, Is.EqualTo(media.MimeType)); // Changed MineType to MimeType + Assert.That(fileResult?.FileDownloadName, Is.EqualTo(Path.GetFileName(media.FileName))); + } + + [Test] + public async Task Download_NoAccess_ReturnsForbid() + { + // Arrange + var mediaId = Guid.NewGuid(); + + _mockAccesser.Setup(f => f.HasAccessAsync(mediaId, _userId)).ReturnsAsync(false); + + // Act + var result = await _controller.Download(mediaId); + + // Assert + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public async Task Download_MediaNotFound_ReturnsNotFound() + { + // Arrange + var mediaId = Guid.NewGuid(); + + _mockAccesser.Setup(f => f.HasAccessAsync(mediaId, _userId)).ReturnsAsync(true); + _mockMedia.Setup(f => f.GetMediaByIdAsync(mediaId)) + .ThrowsAsync(new KeyNotFoundException("Media not found")); + + // Act + var result = await _controller.Download(mediaId); + + // Assert + Assert.That(result, Is.InstanceOf()); + var notFoundResult = result as NotFoundObjectResult; + Assert.That(notFoundResult?.Value, Is.EqualTo("Media not found")); + } + + [Test] + public async Task Download_GeneralException_ReturnsInternalServerError() + { + // Arrange + var mediaId = Guid.NewGuid(); + + _mockAccesser.Setup(f => f.HasAccessAsync(mediaId, _userId)).ReturnsAsync(true); + _mockMedia.Setup(f => f.GetMediaByIdAsync(mediaId)) + .ThrowsAsync(new Exception("Something went wrong")); + + // Act + var result = await _controller.Download(mediaId); + + // Assert + Assert.That(result, Is.InstanceOf()); + var objectResult = result as ObjectResult; + Assert.That(objectResult.StatusCode, Is.EqualTo(500)); + } [TearDown] public void TearDown() diff --git a/Govor.API/Controllers/MediaController.cs b/Govor.API/Controllers/MediaController.cs index d9ada27..e48e45d 100644 --- a/Govor.API/Controllers/MediaController.cs +++ b/Govor.API/Controllers/MediaController.cs @@ -102,7 +102,7 @@ public class MediaController : Controller var media = await _mediaService.GetMediaByIdAsync(id); - return File(media.Data, media.MineType, Path.GetFileName(media.FileName)); + return File(media.Data, media.MimeType, Path.GetFileName(media.FileName)); } catch (KeyNotFoundException ex) { diff --git a/Govor.API/Hubs/ChatsHub.cs b/Govor.API/Hubs/ChatsHub.cs index 4869472..2f50ea8 100644 --- a/Govor.API/Hubs/ChatsHub.cs +++ b/Govor.API/Hubs/ChatsHub.cs @@ -30,16 +30,16 @@ public class ChatsHub : Hub if (userId == Guid.Empty) { _logger.LogWarning("User connected with invalid UserID claim."); - Context.Abort(); // Abort connection if userID is invalid + Context.Abort(); return; } - - // Add user to their own group (for private messages and notifications) + await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString()); _logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId} and added to their group", userId, Context.ConnectionId); var userGroups = await _userService.GetUserGroupsAsync(userId); + foreach (var group in userGroups) { await Groups.AddToGroupAsync(Context.ConnectionId, $"group_{group.Id}"); @@ -51,19 +51,17 @@ public class ChatsHub : Hub public override async Task OnDisconnectedAsync(Exception? exception) { var userId = - GetUserId(suppressException: true); // Suppress exception if userID is not found (e.g. connection aborted early) + GetUserId(suppressException: true); if (userId != Guid.Empty) { - // Remove user from their own group await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString()); - _logger.LogInformation( - "User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group", userId, - Context.ConnectionId); + _logger.LogInformation("User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group", + userId, Context.ConnectionId); var userGroups = await _userService .GetUserGroupsAsync( - userId); // This might be problematic if the service relies on the user being connected + userId); foreach (var group in userGroups) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"group_{group.Id}"); diff --git a/Govor.API/Hubs/FriendsHub.cs b/Govor.API/Hubs/FriendsHub.cs index b252080..3fa025e 100644 --- a/Govor.API/Hubs/FriendsHub.cs +++ b/Govor.API/Hubs/FriendsHub.cs @@ -33,11 +33,10 @@ public class FriendsHub : Hub if (userId == Guid.Empty) { _logger.LogWarning("User connected with invalid UserID claim."); - Context.Abort(); // Abort connection if userID is invalid + Context.Abort(); return; } - - // Add user to their own group (for private messages and notifications) + await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString()); _logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId} and added to their group", userId, Context.ConnectionId); @@ -48,7 +47,7 @@ public class FriendsHub : Hub public override async Task OnDisconnectedAsync(Exception? exception) { var userId = - GetUserId(suppressException: true); // Suppress exception if userID is not found (e.g. connection aborted early) + GetUserId(suppressException: true); if (userId != Guid.Empty) { // Remove user from their own group diff --git a/Govor.Application/Interfaces/Medias/IMediaService.cs b/Govor.Application/Interfaces/Medias/IMediaService.cs index b8a9735..30663ad 100644 --- a/Govor.Application/Interfaces/Medias/IMediaService.cs +++ b/Govor.Application/Interfaces/Medias/IMediaService.cs @@ -15,7 +15,7 @@ public record Media(Guid UploaderId, string FileName, byte[] Data, MediaType Type, - string MineType, + string MimeType, string EncryptedKey); public record MediaUploadResult(Guid? MediaId, string Url); \ No newline at end of file diff --git a/Govor.Application/Interfaces/Messages/IMessageCommandService.cs b/Govor.Application/Interfaces/Messages/IMessageCommandService.cs index e38aa15..b927074 100644 --- a/Govor.Application/Interfaces/Messages/IMessageCommandService.cs +++ b/Govor.Application/Interfaces/Messages/IMessageCommandService.cs @@ -15,7 +15,7 @@ public interface IMessageCommandService // Task MarkMessageAsReadAsync(Guid userId, Guid messageId); } -// Define specific result types for clarity, including original message for notifications if needed + public record SendMessageResult(bool IsSuccess, Exception? Exception, Message Message) : Result(IsSuccess, Exception, Message?.Id ?? Guid.Empty); @@ -23,7 +23,7 @@ public record SendMessageResult(bool IsSuccess, Exception? Exception, Message Me public record EditMessageResult(bool IsSuccess, Exception? Exception, Message? OriginalMessage) : Result(IsSuccess, Exception, OriginalMessage?.Id ?? Guid.Empty) { - // OriginalMessage can be useful for the Hub to know details like RecipientType, RecipientId for notifications + } public record DeleteMessageResult(bool IsSuccess, Exception? Exception, Message? OriginalMessage) diff --git a/Govor.Application/Services/Medias/MediaService.cs b/Govor.Application/Services/Medias/MediaService.cs index a8e9ff9..8e62ed6 100644 --- a/Govor.Application/Services/Medias/MediaService.cs +++ b/Govor.Application/Services/Medias/MediaService.cs @@ -34,7 +34,7 @@ public class MediaService : IMediaService UploaderId = file.UploaderId, DateCreated = file.UploadedOn, MediaType = file.Type, - MineType = file.MineType, + MineType = file.MimeType, Url = url }); diff --git a/Govor.Application/Services/Messages/MessageCommandService.cs b/Govor.Application/Services/Messages/MessageCommandService.cs index 09b59d1..abaa464 100644 --- a/Govor.Application/Services/Messages/MessageCommandService.cs +++ b/Govor.Application/Services/Messages/MessageCommandService.cs @@ -15,10 +15,10 @@ namespace Govor.Application.Services.Messages; public class MessageCommandService : IMessageCommandService { private readonly IMessagesRepository _messagesRepository; - private readonly IUsersRepository _usersRepository; // For validating user recipients - private readonly IGroupsRepository _groupsRepository; // For validating group recipients and fetching members + private readonly IUsersRepository _usersRepository; + private readonly IGroupsRepository _groupsRepository; private readonly IPrivateChatsRepository _privateChats; - private readonly IVerifyFriendship _verifyFriendship; // For private messages + private readonly IVerifyFriendship _verifyFriendship; private readonly ILogger _logger; public MessageCommandService(