diff --git a/.github/workflows/azure_deploy.yml b/.github/workflows/azure_deploy.yml
new file mode 100644
index 0000000..e3e17ba
--- /dev/null
+++ b/.github/workflows/azure_deploy.yml
@@ -0,0 +1,50 @@
+name: Build, Test, and Deploy to Azure
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+
+env:
+ AZURE_WEBAPP_NAME: chat-service-ali-nadim
+ AZURE_WEBAPP_PACKAGE_PATH: './publish'
+
+jobs:
+ build-and-deploy:
+ runs-on: ubuntu-latest
+
+ steps:
+
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: '6.0.x'
+
+ - name: Restore dependencies
+ run: dotnet restore ./ChatService.sln
+
+ - name: Build
+ run: dotnet build ./ChatService.sln --configuration Release --no-restore
+
+ - name: Run unit tests
+ run: dotnet test ChatService.Web.Tests/bin/Release/net6.0/ChatService.Web.Tests.dll
+
+ - name: Run integration tests
+ run: dotnet test ChatService.Web.IntegrationTests/bin/Release/net6.0/ChatService.Web.IntegrationTests.dll
+ env:
+ Cosmos:ConnectionString: ${{ secrets.COSMOS_CONNECTIONSTRING }}
+ BlobStorage:ConnectionString: ${{ secrets.BLOBSTORAGE_CONNECTIONSTRING }}
+
+ - name: Publish
+ run: dotnet publish --configuration Release --output '${{ env.AZURE_WEBAPP_PACKAGE_PATH }}' --no-restore ChatService.Web
+
+ - name: Deploy
+ uses: azure/webapps-deploy@v2
+ with:
+ app-name: ${{ env.AZURE_WEBAPP_NAME }}
+ publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_CHATSERVICE }}
+ package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}
diff --git a/.github/workflows/prbuild.yml b/.github/workflows/prbuild.yml
new file mode 100644
index 0000000..71fe75b
--- /dev/null
+++ b/.github/workflows/prbuild.yml
@@ -0,0 +1,35 @@
+name: PR Build
+
+on:
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build-and-test:
+
+ runs-on: ubuntu-latest
+
+ steps:
+
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 6.0.x
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --configuration Release --no-restore
+
+ - name: Run unit tests
+ run: dotnet test ChatService.Web.Tests/bin/Release/net6.0/ChatService.Web.Tests.dll
+
+ - name: Run integration tests
+ run: dotnet test ChatService.Web.IntegrationTests/bin/Release/net6.0/ChatService.Web.IntegrationTests.dll
+ env:
+ Cosmos:ConnectionString: ${{ secrets.COSMOS_CONNECTIONSTRING }}
+ BlobStorage:ConnectionString: ${{ secrets.BLOBSTORAGE_CONNECTIONSTRING }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..26958af
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+bin/
+obj/
+/packages/
+riderModule.iml
+/_ReSharper.Caches/
+ChatService.sln.DotSettings.user
\ No newline at end of file
diff --git a/.idea/.idea.ChatService/.idea/.gitignore b/.idea/.idea.ChatService/.idea/.gitignore
new file mode 100644
index 0000000..68a553e
--- /dev/null
+++ b/.idea/.idea.ChatService/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/contentModel.xml
+/.idea.ChatService.iml
+/modules.xml
+/projectSettingsUpdater.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/.idea.ChatService/.idea/indexLayout.xml b/.idea/.idea.ChatService/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/.idea/.idea.ChatService/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.ChatService/.idea/vcs.xml b/.idea/.idea.ChatService/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/.idea.ChatService/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ChatService.Web.IntegrationTests/BlobImageStoreTests.cs b/ChatService.Web.IntegrationTests/BlobImageStoreTests.cs
new file mode 100644
index 0000000..babf34d
--- /dev/null
+++ b/ChatService.Web.IntegrationTests/BlobImageStoreTests.cs
@@ -0,0 +1,99 @@
+using System.Text;
+using Azure;
+using ChatService.Web.Dtos;
+using ChatService.Web.Storage;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ChatService.Web.IntegrationTests;
+
+public class BlobImageStoreTests : IClassFixture>, IAsyncLifetime
+{
+ private readonly IImageStore _imageStore;
+ private readonly Image _image = new Image("image/jpg",
+ new MemoryStream(Encoding.UTF8.GetBytes("This is a mock image file content")));
+ private string _imageId;
+
+ public Task InitializeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _imageStore.DeleteImage(_imageId);
+ }
+
+ public BlobImageStoreTests(WebApplicationFactory factory)
+ {
+ _imageStore = factory.Services.GetRequiredService();
+ }
+
+ [Fact]
+ public async Task UploadImage_Success()
+ {
+ string imageId = await _imageStore.UploadImage(_image);
+ var downloadedImage = await _imageStore.DownloadImage(imageId);
+
+ Assert.Equal(_image.ContentType, downloadedImage.ContentType);
+ Assert.True(_image.Content.ToArray().SequenceEqual(downloadedImage.Content.ToArray()));
+
+ _imageId = imageId;
+ }
+
+ [Fact]
+ public async Task UploadImage_Failure()
+ {
+ var notImage = new Image("text/plain",
+ new MemoryStream(Encoding.UTF8.GetBytes("This is a mock file simulating an invalid image type")));
+
+ await Assert.ThrowsAsync(async () => await _imageStore.UploadImage(notImage));
+ }
+
+ [Fact]
+ public async Task DownloadImage_Success()
+ {
+ var imageId = await _imageStore.UploadImage(_image);
+ var downloadedImage = await _imageStore.DownloadImage(imageId);
+
+ Assert.Equal(_image.ContentType, downloadedImage.ContentType);
+ Assert.True(_image.Content.ToArray().SequenceEqual(downloadedImage.Content.ToArray()));
+ }
+
+ [Fact]
+ public async Task DownloadImage_NotFound()
+ {
+ var downloadedImage = await _imageStore.DownloadImage("dummy_id");
+
+ Assert.Null(downloadedImage);
+ }
+
+ [Fact]
+ public async Task DeleteImage_Success()
+ {
+ var imageId = await _imageStore.UploadImage(_image);
+ Assert.True(await _imageStore.DeleteImage(imageId));
+ }
+
+ [Fact]
+ public async Task DeleteImage_Failure()
+ {
+ Assert.False(await _imageStore.DeleteImage("dummy_id"));
+ }
+
+ [Fact]
+ public async Task ImageExists_Exists()
+ {
+ string imageId = await _imageStore.UploadImage(_image);
+
+ Assert.True(await _imageStore.ImageExists(imageId));
+
+ _imageId = imageId;
+ }
+
+ [Fact]
+ public async Task ImageExists_DoesntExist()
+ {
+ Assert.False(await _imageStore.ImageExists("dummy_id"));
+ }
+}
\ No newline at end of file
diff --git a/ChatService.Web.IntegrationTests/ChatService.Web.IntegrationTests.csproj b/ChatService.Web.IntegrationTests/ChatService.Web.IntegrationTests.csproj
new file mode 100644
index 0000000..3f79c19
--- /dev/null
+++ b/ChatService.Web.IntegrationTests/ChatService.Web.IntegrationTests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/ChatService.Web.IntegrationTests/CosmosConversationStoreTests.cs b/ChatService.Web.IntegrationTests/CosmosConversationStoreTests.cs
new file mode 100644
index 0000000..d60f705
--- /dev/null
+++ b/ChatService.Web.IntegrationTests/CosmosConversationStoreTests.cs
@@ -0,0 +1,259 @@
+using ChatService.Web.Dtos;
+using ChatService.Web.Enums;
+using ChatService.Web.Exceptions;
+using ChatService.Web.Storage;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ChatService.Web.IntegrationTests;
+
+public class CosmosConversationStoreTests : IClassFixture>, IAsyncLifetime
+{
+ private readonly IUserConversationStore _userConversationStore;
+
+ private static readonly UserConversation _userConversation = new UserConversation
+ {
+ Username = Guid.NewGuid().ToString(),
+ ConversationId = Guid.NewGuid().ToString() + "_",
+ LastModifiedTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
+ };
+
+ private readonly UserConversation _userConversation1 = new UserConversation
+ {
+ Username = _userConversation.Username,
+ ConversationId = Guid.NewGuid().ToString() + "_",
+ LastModifiedTime = 100
+ };
+
+ private readonly UserConversation _userConversation2 = new UserConversation
+ {
+ Username = _userConversation.Username,
+ ConversationId = Guid.NewGuid().ToString() + "_",
+ LastModifiedTime = 200
+ };
+
+ private readonly UserConversation _userConversation3 = new UserConversation
+ {
+ Username = _userConversation.Username,
+ ConversationId = Guid.NewGuid().ToString() + "_",
+ LastModifiedTime = 300
+ };
+
+ public CosmosConversationStoreTests(WebApplicationFactory factory)
+ {
+ _userConversationStore = factory.Services.GetRequiredService();
+ }
+
+ [Fact]
+ public async Task CreateUserConversation_Successful()
+ {
+ await _userConversationStore.CreateUserConversation(_userConversation);
+
+ Assert.Equal(_userConversation, await _userConversationStore.GetUserConversation(_userConversation.Username, _userConversation.ConversationId));
+ }
+
+ [Theory]
+ [InlineData(null, "dummy_conversationId", 100)]
+ [InlineData("", "dummy_conversationId", 100)]
+ [InlineData(" ", "dummy_conversationId", 100)]
+ [InlineData("foobar", null, 100)]
+ [InlineData("foobar", "", 100)]
+ [InlineData("foobar", " ", 100)]
+ [InlineData("foobar", "dummy_conversationId", -100)]
+ public async Task CreateUserConversation_InvalidArguments(string username, string conversationId, long lastModifiedTime)
+ {
+ UserConversation userConversation = new()
+ {
+ Username = username,
+ ConversationId = conversationId,
+ LastModifiedTime = lastModifiedTime
+ };
+
+ await Assert.ThrowsAsync(
+ () => _userConversationStore.CreateUserConversation(userConversation));
+ }
+
+ [Fact]
+ public async Task CreateUserConversation_ConversationAlreadyExists()
+ {
+ await _userConversationStore.CreateUserConversation(_userConversation);
+
+ await Assert.ThrowsAsync(
+ () => _userConversationStore.CreateUserConversation(_userConversation));
+ }
+
+ [Theory]
+ [InlineData(null, "dummy_conversationId")]
+ [InlineData("", "dummy_conversationId")]
+ [InlineData(" ", "dummy_conversationId")]
+ [InlineData("foobar", null)]
+ [InlineData("foobar", "")]
+ [InlineData("foobar", " ")]
+ public async Task GetUserConversation_InvalidArguments(string username, string conversationId)
+ {
+ await Assert.ThrowsAsync(
+ () => _userConversationStore.GetUserConversation(username, conversationId));
+ }
+
+ [Fact]
+ public async Task GetUserConversation_ConversationNotFound()
+ {
+ await Assert.ThrowsAsync(
+ () => _userConversationStore.GetUserConversation(_userConversation.Username, _userConversation.ConversationId));
+ }
+
+ [Fact]
+ public async Task GetUserConversations_Limit()
+ {
+ await AddMultipleUserConversations(_userConversation1, _userConversation2, _userConversation3);
+
+ var response = await _userConversationStore.GetUserConversations(_userConversation.Username, 1, OrderBy.ASC, null, 1);
+ Assert.Equal(1, response.UserConversations.Count);
+
+ response = await _userConversationStore.GetUserConversations(_userConversation.Username, 2, OrderBy.ASC, null, 1);
+ Assert.Equal(2, response.UserConversations.Count);
+
+ response = await _userConversationStore.GetUserConversations(_userConversation.Username, 3, OrderBy.ASC, null, 1);
+ Assert.Equal(3, response.UserConversations.Count);
+
+ await DeleteMultipleUserConversations(_userConversation1, _userConversation2, _userConversation3);
+ }
+
+ [Theory]
+ [InlineData(OrderBy.ASC)]
+ [InlineData(OrderBy.DESC)]
+ public async Task GetUserConversations_OrderBy(OrderBy orderBy)
+ {
+ List userConversationsExpected = CreateListOfUserConversations(
+ _userConversation1, _userConversation2, _userConversation3, _userConversation);
+
+ await AddMultipleUserConversations(
+ _userConversation, _userConversation1, _userConversation2, _userConversation3);
+
+ var response = await _userConversationStore.GetUserConversations(_userConversation.Username, 10, orderBy, null, 0);
+
+ if (orderBy == OrderBy.ASC)
+ {
+ Assert.Equal(userConversationsExpected, response.UserConversations);
+ }
+ else
+ {
+ userConversationsExpected.Reverse();
+ Assert.Equal(userConversationsExpected, response.UserConversations);
+ }
+
+ await DeleteMultipleUserConversations(_userConversation1, _userConversation2, _userConversation3);
+ }
+
+ [Fact]
+ public async Task GetUserConversations_ContinuationTokenValidity()
+ {
+ await AddMultipleUserConversations(_userConversation1, _userConversation2, _userConversation3);
+
+ var response = await _userConversationStore.GetUserConversations(
+ _userConversation.Username, 1, OrderBy.ASC, null, 1);
+
+ Assert.Equal(_userConversation1, response.UserConversations.ElementAt(0));
+
+ var nextContinuation = response.NextContinuationToken;
+ Assert.NotNull(nextContinuation);
+
+ response = await _userConversationStore.GetUserConversations(_userConversation.Username, 1, OrderBy.ASC, nextContinuation, 1);
+ Assert.Equal(_userConversation2, response.UserConversations.ElementAt(0));
+
+ nextContinuation = response.NextContinuationToken;
+ Assert.NotNull(nextContinuation);
+
+ response = await _userConversationStore.GetUserConversations(_userConversation.Username, 1, OrderBy.ASC, nextContinuation, 1);
+ Assert.Equal(_userConversation3, response.UserConversations.ElementAt(0));
+
+ nextContinuation = response.NextContinuationToken;
+ Assert.Null(nextContinuation);
+
+ await DeleteMultipleUserConversations(_userConversation1, _userConversation2, _userConversation3);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(100)]
+ [InlineData(200)]
+ [InlineData(300)]
+ public async Task GetUserConversations_LastSeenConversationTime(long lastSeenConversationTime)
+ {
+ await AddMultipleUserConversations(_userConversation1, _userConversation2, _userConversation3, _userConversation);
+
+ List userConversationsExpected = new();
+
+ if(_userConversation1.LastModifiedTime > lastSeenConversationTime) { userConversationsExpected.Add(_userConversation1); }
+ if(_userConversation2.LastModifiedTime > lastSeenConversationTime) { userConversationsExpected.Add(_userConversation2);}
+ if(_userConversation3.LastModifiedTime > lastSeenConversationTime) { userConversationsExpected.Add(_userConversation3);}
+ if(_userConversation.LastModifiedTime > lastSeenConversationTime) { userConversationsExpected.Add(_userConversation);}
+
+ var response = await _userConversationStore.GetUserConversations(_userConversation.Username, 10, OrderBy.ASC, null, lastSeenConversationTime);
+
+ Assert.Equal(userConversationsExpected, response.UserConversations);
+
+ await DeleteMultipleUserConversations(_userConversation1, _userConversation2, _userConversation3);
+ }
+
+ [Theory]
+ [InlineData("", 1, 100)]
+ [InlineData(" ", 1, 100)]
+ [InlineData(null, 0, 100)]
+ [InlineData("username", 0, 100)]
+ [InlineData("username", -1, 100)]
+ [InlineData("username", 10, -100)]
+ public async Task GetUserConversations_InvalidArguments(string username, int limit, long lastSeenConversationTime)
+ {
+ await Assert.ThrowsAsync(
+ () => _userConversationStore.GetUserConversations(username, limit, OrderBy.ASC, null, lastSeenConversationTime));
+ }
+
+ [Fact]
+ public async Task GetUserConversations_InvalidContinuationToken()
+ {
+ string invalidContinuationToken = Guid.NewGuid().ToString();
+
+ await Assert.ThrowsAsync(
+ () => _userConversationStore.GetUserConversations(
+ _userConversation.Username, 10, OrderBy.DESC, invalidContinuationToken, 0));
+ }
+
+ private async Task AddMultipleUserConversations(params UserConversation[] userConversations)
+ {
+ foreach (UserConversation userConversation in userConversations)
+ {
+ await _userConversationStore.CreateUserConversation(userConversation);
+ }
+ }
+
+ private List CreateListOfUserConversations(params UserConversation[] userConversations)
+ {
+ List list = new();
+
+ foreach (UserConversation userConversation in userConversations)
+ {
+ list.Add(userConversation);
+ }
+
+ return list;
+ }
+
+ private async Task DeleteMultipleUserConversations(params UserConversation[] userConversations)
+ {
+ foreach (UserConversation userConversation in userConversations)
+ {
+ await _userConversationStore.DeleteUserConversation(userConversation.Username, userConversation.ConversationId);
+ }
+ }
+
+ public Task InitializeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _userConversationStore.DeleteUserConversation(_userConversation.Username, _userConversation.ConversationId);
+ }
+}
\ No newline at end of file
diff --git a/ChatService.Web.IntegrationTests/CosmosMessageStoreTests.cs b/ChatService.Web.IntegrationTests/CosmosMessageStoreTests.cs
new file mode 100644
index 0000000..bce1098
--- /dev/null
+++ b/ChatService.Web.IntegrationTests/CosmosMessageStoreTests.cs
@@ -0,0 +1,255 @@
+using ChatService.Web.Dtos;
+using ChatService.Web.Enums;
+using ChatService.Web.Exceptions;
+using ChatService.Web.Storage;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ChatService.Web.IntegrationTests;
+
+public class CosmosMessageStoreTests : IClassFixture>, IAsyncLifetime
+{
+ private readonly IMessageStore _messageStore;
+
+ private readonly string _conversationId = Guid.NewGuid().ToString() + "_";
+
+ private readonly Message _message1 = new Message
+ {
+ MessageId = Guid.NewGuid().ToString(),
+ UnixTime = 100,
+ SenderUsername = Guid.NewGuid().ToString(),
+ Text = "text of _message1"
+ };
+
+ private readonly Message _message2 = new Message
+ {
+ MessageId = Guid.NewGuid().ToString(),
+ UnixTime = 200,
+ SenderUsername = Guid.NewGuid().ToString(),
+ Text = "text of _message2"
+ };
+
+ private readonly Message _message3 = new Message
+ {
+ MessageId = Guid.NewGuid().ToString(),
+ UnixTime = 300,
+ SenderUsername = Guid.NewGuid().ToString(),
+ Text = "text of _message3"
+ };
+
+ public CosmosMessageStoreTests(WebApplicationFactory factory)
+ {
+ _messageStore = factory.Services.GetRequiredService();
+ }
+
+ [Fact]
+ public async Task AddMessage_Successful()
+ {
+ await _messageStore.AddMessage(_conversationId, _message1);
+
+ Assert.Equal(_message1, await _messageStore.GetMessage(_conversationId, _message1.MessageId));
+ }
+
+ [Theory]
+ [InlineData(null, "senderUsername", "text", 100)]
+ [InlineData("", "senderUsername", "text", 100)]
+ [InlineData(" ", "senderUsername", "text", 100)]
+ [InlineData("id", null, "text", 100)]
+ [InlineData("id", "", "text", 100)]
+ [InlineData("id", " ", "text", 100)]
+ [InlineData("id", "senderUsername", null, 100)]
+ [InlineData("id", "senderUsername", "", 100)]
+ [InlineData("id", "senderUsername", " ", 100)]
+ [InlineData("id", "senderUsername", "text", -100)]
+ public async Task AddMessage_InvalidArguments(string id, string senderUsername, string text, long unixTime)
+ {
+ Message message = new Message
+ {
+ MessageId = id,
+ UnixTime = unixTime,
+ SenderUsername = senderUsername,
+ Text = text
+ };
+
+ await Assert.ThrowsAsync(() => _messageStore.AddMessage(_conversationId, message));
+ }
+
+ [Fact]
+ public async Task AddMessage_MessageAlreadyExists()
+ {
+ await _messageStore.AddMessage(_conversationId, _message1);
+
+ await Assert.ThrowsAsync(() => _messageStore.AddMessage(_conversationId, _message1));
+ }
+
+ [Theory]
+ [InlineData(null, "messageId")]
+ [InlineData("", "messageId")]
+ [InlineData(" ", "messageId")]
+ [InlineData("conversationId", null)]
+ [InlineData("conversationId", "")]
+ [InlineData("conversationId", " ")]
+ public async Task GetMessage_InvalidArguments(string conversationId, string messageId)
+ {
+ await Assert.ThrowsAsync(() => _messageStore.GetMessage(conversationId, messageId));
+ }
+
+ [Fact]
+ public async Task GetMessage_MessageNotFound()
+ {
+ await Assert.ThrowsAsync(() => _messageStore.GetMessage(_conversationId, _message1.MessageId));
+ }
+
+ [Fact]
+ public async Task GetMessages_Limit()
+ {
+ await AddMultipleMessages(_conversationId, _message1, _message2, _message3);
+
+ var response = await _messageStore.GetMessages(
+ _conversationId, 1, OrderBy.ASC, null, 1);
+ Assert.Equal(1, response.Messages.Count);
+
+ response = await _messageStore.GetMessages(_conversationId, 2, OrderBy.ASC, null, 1);
+ Assert.Equal(2, response.Messages.Count);
+
+ response = await _messageStore.GetMessages(_conversationId, 3, OrderBy.ASC, null, 1);
+ Assert.Equal(3, response.Messages.Count);
+
+ await DeleteMultipleMessages(_conversationId, _message1, _message2, _message3);
+ }
+
+ [Theory]
+ [InlineData(OrderBy.ASC)]
+ [InlineData(OrderBy.DESC)]
+ public async Task GetMessages_OrderBy(OrderBy orderBy)
+ {
+ await AddMultipleMessages(_conversationId, _message1, _message2);
+
+ List messagesExpected = new();
+ messagesExpected.Add(_message1);
+ messagesExpected.Add(_message2);
+
+ var response = await _messageStore.GetMessages(
+ _conversationId, 10, orderBy, null, 1);
+
+ if (orderBy == OrderBy.ASC)
+ {
+ Assert.Equal(messagesExpected, response.Messages);
+ }
+ else
+ {
+ messagesExpected.Reverse();
+ Assert.Equal(messagesExpected, response.Messages);
+ }
+
+ await _messageStore.DeleteMessage(_conversationId, _message2.MessageId);
+ }
+
+ [Fact]
+ public async Task GetMessages_ContinuationTokenValidity()
+ {
+ await AddMultipleMessages(_conversationId, _message1, _message2, _message3);
+
+ var response = await _messageStore.GetMessages(
+ _conversationId, 1, OrderBy.ASC, null, 1);
+ Assert.Equal(_message1, response.Messages.ElementAt(0));
+ Assert.NotNull(response.NextContinuationToken);
+
+ response = await _messageStore.GetMessages(
+ _conversationId, 1, OrderBy.ASC, response.NextContinuationToken, 1);
+ Assert.Equal(_message2, response.Messages.ElementAt(0));
+ Assert.NotNull(response.NextContinuationToken);
+
+ response = await _messageStore.GetMessages(
+ _conversationId, 1, OrderBy.ASC, response.NextContinuationToken, 1);
+ Assert.Equal(_message3, response.Messages.ElementAt(0));
+ Assert.Null(response.NextContinuationToken);
+
+ await DeleteMultipleMessages(_conversationId, _message2, _message3);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(100)]
+ [InlineData(200)]
+ [InlineData(300)]
+ public async Task GetMessages_LastSeenMessageTime(long lastSeenMessageTime)
+ {
+ await AddMultipleMessages(_conversationId, _message1, _message2, _message3);
+
+ List messagesExpected = new();
+ if(_message1.UnixTime > lastSeenMessageTime) messagesExpected.Add(_message1);
+ if(_message2.UnixTime > lastSeenMessageTime) messagesExpected.Add(_message2);
+ if(_message3.UnixTime > lastSeenMessageTime) messagesExpected.Add(_message3);
+
+ var response = await _messageStore.GetMessages(
+ _conversationId, 10, OrderBy.ASC, null, lastSeenMessageTime);
+
+ Assert.Equal(messagesExpected, response.Messages);
+
+ await DeleteMultipleMessages(_conversationId, _message2, _message3);
+ }
+
+ [Theory]
+ [InlineData(null, 10, 100)]
+ [InlineData("", 10, 100)]
+ [InlineData(" ", 10, 100)]
+ [InlineData("conversationId", 0, 100)]
+ [InlineData("conversationId", -10, 100)]
+ [InlineData("conversationId", 10, -100)]
+ public async Task GetMessages_InvalidArguments(string conversationId, int limit, long lastSeenMessageTime)
+ {
+ Assert.ThrowsAsync(() =>
+ _messageStore.GetMessages(conversationId, limit, OrderBy.ASC, null, lastSeenMessageTime));
+ }
+
+ [Fact]
+ public async Task GetMessages_InvalidContinuationToken()
+ {
+ string invalidContinuationToken = Guid.NewGuid().ToString();
+
+ await Assert.ThrowsAsync(
+ () => _messageStore.GetMessages(
+ _conversationId, 10, OrderBy.DESC, invalidContinuationToken, 0));
+ }
+
+ [Fact]
+ public async Task ConversationPartitionExists_Exists()
+ {
+ await _messageStore.AddMessage(_conversationId, _message1);
+
+ Assert.True(await _messageStore.ConversationPartitionExists(_conversationId));
+ }
+
+ [Fact]
+ public async Task ConversationPartitionExists_DoesNotExists()
+ {
+ Assert.False(await _messageStore.ConversationPartitionExists(_conversationId));
+ }
+
+ private async Task AddMultipleMessages(string conversationId, params Message[] messages)
+ {
+ foreach (Message message in messages)
+ {
+ await _messageStore.AddMessage(conversationId, message);
+ }
+ }
+
+ private async Task DeleteMultipleMessages(string conversationId, params Message[] messages)
+ {
+ foreach (Message message in messages)
+ {
+ await _messageStore.DeleteMessage(conversationId, message.MessageId);
+ }
+ }
+
+ public Task InitializeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _messageStore.DeleteMessage(_conversationId, _message1.MessageId);
+ }
+}
\ No newline at end of file
diff --git a/ChatService.Web.IntegrationTests/CosmosProfileStoreTests.cs b/ChatService.Web.IntegrationTests/CosmosProfileStoreTests.cs
new file mode 100644
index 0000000..3395041
--- /dev/null
+++ b/ChatService.Web.IntegrationTests/CosmosProfileStoreTests.cs
@@ -0,0 +1,109 @@
+using ChatService.Web.Dtos;
+using ChatService.Web.Exceptions;
+using ChatService.Web.Storage;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ChatService.Web.IntegrationTests;
+
+public class CosmosProfileStoreTest : IClassFixture>, IAsyncLifetime
+{
+ private readonly IProfileStore _profileStore;
+
+ private readonly Profile _profile = new Profile
+ {
+ Username = Guid.NewGuid().ToString(),
+ FirstName = "Foo",
+ LastName = "Bar",
+ ProfilePictureId = "dummy_id"
+ };
+
+ public Task InitializeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _profileStore.DeleteProfile(_profile.Username);
+ }
+
+ public CosmosProfileStoreTest(WebApplicationFactory factory)
+ {
+ _profileStore = factory.Services.GetRequiredService();
+ }
+
+ [Fact]
+ public async Task AddNewProfile_Success()
+ {
+ await _profileStore.AddProfile(_profile);
+ Assert.Equal(_profile, await _profileStore.GetProfile(_profile.Username));
+ }
+
+ [Theory]
+ [InlineData(null, "Foo", "Bar", "dummy_id")]
+ [InlineData("", "Foo", "Bar", "dummy_id")]
+ [InlineData(" ", "Foo", "Bar", "dummy_id")]
+ [InlineData("foobar", null, "Bar", "dummy_id")]
+ [InlineData("foobar", "", "Bar", "dummy_id")]
+ [InlineData("foobar", " ", "Bar", "dummy_id")]
+ [InlineData("foobar", "Foo", null, "dummy_id")]
+ [InlineData("foobar", "Foo", "", "dummy_id")]
+ [InlineData("foobar", "Foo", " ", "dummy_id")]
+ [InlineData("foobar", "Foo", "Bar", null)]
+ [InlineData("foobar", "Foo", "Bar","")]
+ [InlineData("foobar", "Foo", "Bar"," ")]
+ public async Task AddNewProfile_InvalidArgs(string username, string firstName, string lastName, string profilePictureId)
+ {
+ Profile profile = new Profile
+ {
+ Username = username,
+ FirstName = firstName,
+ LastName = lastName,
+ ProfilePictureId = profilePictureId
+ };
+
+ await Assert.ThrowsAsync( async () => await _profileStore.AddProfile(profile));
+ }
+
+ [Fact]
+ public async Task AddNewProfile_NullProfile()
+ {
+ await Assert.ThrowsAsync( async () => await _profileStore.AddProfile(null));
+ }
+
+ [Fact]
+ public async Task AddNewProfile_UsernameTaken()
+ {
+ await _profileStore.AddProfile(_profile);
+ await Assert.ThrowsAsync( async () => await _profileStore.AddProfile(_profile));
+ }
+
+ [Fact]
+ public async Task GetNonExistingProfile()
+ {
+ Assert.Null(await _profileStore.GetProfile(_profile.Username));
+ }
+
+ [Fact]
+ public async Task DeleteProfile()
+ {
+ await _profileStore.AddProfile(_profile);
+ Assert.Equal(_profile, await _profileStore.GetProfile(_profile.Username));
+ await _profileStore.DeleteProfile(_profile.Username);
+ Assert.Null(await _profileStore.GetProfile(_profile.Username));
+ }
+
+ [Fact]
+ public async Task ProfileExists_Exists()
+ {
+ await _profileStore.AddProfile(_profile);
+ Assert.True(await _profileStore.ProfileExists(_profile.Username));
+ }
+
+ [Fact]
+ public async Task ProfileExists_DoesNotExist()
+ {
+ Assert.False(await _profileStore.ProfileExists(_profile.Username));
+ }
+}
\ No newline at end of file
diff --git a/ChatService.Web.IntegrationTests/Usings.cs b/ChatService.Web.IntegrationTests/Usings.cs
new file mode 100644
index 0000000..8c927eb
--- /dev/null
+++ b/ChatService.Web.IntegrationTests/Usings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file
diff --git a/ChatService.Web.Tests/ChatService.Web.Tests.csproj b/ChatService.Web.Tests/ChatService.Web.Tests.csproj
new file mode 100644
index 0000000..d2ff97f
--- /dev/null
+++ b/ChatService.Web.Tests/ChatService.Web.Tests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/ChatService.Web.Tests/Controllers/ConversationsControllerTests.cs b/ChatService.Web.Tests/Controllers/ConversationsControllerTests.cs
new file mode 100644
index 0000000..f233245
--- /dev/null
+++ b/ChatService.Web.Tests/Controllers/ConversationsControllerTests.cs
@@ -0,0 +1,345 @@
+using System.Net;
+using System.Net.Http.Json;
+using ChatService.Web.Dtos;
+using ChatService.Web.Enums;
+using ChatService.Web.Exceptions;
+using ChatService.Web.Services;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Newtonsoft.Json;
+
+namespace ChatService.Web.Tests.Controllers;
+
+public class ConversationsControllerTests : IClassFixture>
+{
+ private readonly HttpClient _httpClient;
+ private readonly Mock _userConversationServiceMock = new();
+ private readonly Mock _messageServiceMock = new();
+
+ private static readonly string _username = Guid.NewGuid().ToString();
+
+ private static readonly SendMessageRequest _sendMessageRequest = new SendMessageRequest
+ {
+ MessageId = Guid.NewGuid().ToString(),
+ SenderUsername = _username,
+ Text = "Hello"
+ };
+
+ private readonly string _conversationId = Guid.NewGuid().ToString();
+
+ private readonly long _unixTimeNow = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+
+ private readonly string _nextContinuationToken = Guid.NewGuid().ToString();
+
+ private readonly StartConversationRequest _startConversationRequest = new StartConversationRequest
+ {
+ Participants = new List { _username, Guid.NewGuid().ToString() },
+ FirstMessage = _sendMessageRequest
+ };
+
+ public ConversationsControllerTests(WebApplicationFactory factory)
+ {
+ _httpClient = factory.WithWebHostBuilder(builder =>
+ {
+ builder.ConfigureTestServices(services =>
+ {
+ services.AddSingleton(_userConversationServiceMock.Object);
+ services.AddSingleton(_messageServiceMock.Object);
+ });
+ }).CreateClient();
+ }
+
+ [Fact]
+ public async Task GetUserConversations_Success()
+ {
+ List conversations = new();
+ conversations.Add(new Conversation
+ {
+ ConversationId = Guid.NewGuid().ToString(),
+ LastModifiedUnixTime = _unixTimeNow
+ });
+ conversations.Add(new Conversation
+ {
+ ConversationId = Guid.NewGuid().ToString(),
+ LastModifiedUnixTime = _unixTimeNow
+ });
+
+ var userConversationServiceResult = new GetUserConversationsServiceResult
+ {
+ Conversations = conversations,
+ NextContinuationToken = _nextContinuationToken
+ };
+
+ _userConversationServiceMock.Setup(m => m.GetUserConversations(_username, 10, OrderBy.DESC, null, 0))
+ .ReturnsAsync(userConversationServiceResult);
+
+ string nextUri = "/api/conversations" +
+ $"?username={_username}" +
+ "&limit=10" +
+ "&lastSeenConversationTime=0" +
+ $"&continuationToken={_nextContinuationToken}";
+
+ var response = await _httpClient.GetAsync($"api/Conversations/?username={_username}");
+ var json = await response.Content.ReadAsStringAsync();
+ var receivedGetUserConversationsResponse = JsonConvert.DeserializeObject(json);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(conversations, receivedGetUserConversationsResponse.Conversations);
+ Assert.Equal(nextUri, receivedGetUserConversationsResponse.NextUri);
+ }
+
+ [Fact]
+ public async Task GetUserConversations_InvalidArguments()
+ {
+ _userConversationServiceMock.Setup(m => m.GetUserConversations(_username, 10, OrderBy.DESC, null, 0))
+ .ThrowsAsync(new ArgumentException($"Invalid arguments."));
+
+ var response = await _httpClient.GetAsync($"api/Conversations/?username={_username}");
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetUserConversations_InvalidContinuationToken()
+ {
+ string invalidContinuationToken = Guid.NewGuid().ToString();
+
+ _userConversationServiceMock.Setup(m => m.GetUserConversations(
+ _username, 10, OrderBy.DESC, invalidContinuationToken, 0))
+ .ThrowsAsync(new InvalidContinuationTokenException($"Continuation token {invalidContinuationToken} is invalid."));
+
+ var response = await _httpClient.GetAsync(
+ $"api/Conversations/?username={_username}&continuationToken={invalidContinuationToken}");
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetUserConversations_UserNotFound()
+ {
+ _userConversationServiceMock.Setup(m => m.GetUserConversations(
+ _username, 10, OrderBy.DESC, null, 0))
+ .ThrowsAsync(new UserNotFoundException($"User {_username} was not found."));
+
+ var response = await _httpClient.GetAsync(
+ $"api/Conversations/?username={_username}&");
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task StartConversation_Success()
+ {
+ var startConversationServiceResult = new StartConversationServiceResult
+ {
+ ConversationId = Guid.NewGuid().ToString(),
+ CreatedUnixTime = _unixTimeNow
+ };
+
+ _userConversationServiceMock.Setup(m => m.CreateConversation(It.Is(
+ p => p.Participants.SequenceEqual(_startConversationRequest.Participants)
+ && p.FirstMessage == _startConversationRequest.FirstMessage)))
+ .ReturnsAsync(startConversationServiceResult);
+
+ var expectedStartConversationResponse = new StartConversationResponse
+ {
+ ConversationId = startConversationServiceResult.ConversationId,
+ CreatedUnixTime = startConversationServiceResult.CreatedUnixTime
+ };
+
+ var response = await _httpClient.PostAsJsonAsync($"api/Conversations/", _startConversationRequest);
+ var json = await response.Content.ReadAsStringAsync();
+ var receivedStartConversationResponse = JsonConvert.DeserializeObject(json);
+
+ Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+ Assert.Equal(expectedStartConversationResponse, receivedStartConversationResponse);
+ }
+
+ [Fact]
+ public async Task StartConversation_InvalidArguments()
+ {
+ _userConversationServiceMock.Setup(m => m.CreateConversation(It.Is(
+ p => p.Participants.SequenceEqual(_startConversationRequest.Participants)
+ && p.FirstMessage == _startConversationRequest.FirstMessage)))
+ .ThrowsAsync(new ArgumentException());
+
+ var response = await _httpClient.PostAsJsonAsync($"api/Conversations/", _startConversationRequest);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task StartConversation_ProfileNotFound()
+ {
+ _userConversationServiceMock.Setup(m => m.CreateConversation(It.Is(
+ p => p.Participants.SequenceEqual(_startConversationRequest.Participants)
+ && p.FirstMessage == _startConversationRequest.FirstMessage)))
+ .ThrowsAsync(new ProfileNotFoundException($"A profile with the username {_username} was not found."));
+
+ var response = await _httpClient.PostAsJsonAsync($"api/Conversations/", _startConversationRequest);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task StartConversation_MessageExists()
+ {
+ _userConversationServiceMock.Setup(m => m.CreateConversation(It.Is(
+ p => p.Participants.SequenceEqual(_startConversationRequest.Participants)
+ && p.FirstMessage == _startConversationRequest.FirstMessage)))
+ .ThrowsAsync(new MessageExistsException(
+ $"A message with ID {_startConversationRequest.FirstMessage.MessageId} already exists."));
+
+ var response = await _httpClient.PostAsJsonAsync($"api/Conversations/", _startConversationRequest);
+
+ Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetMessages_Success()
+ {
+ List messages = new();
+ messages.Add(new Message
+ {
+ MessageId = Guid.NewGuid().ToString(),
+ SenderUsername = Guid.NewGuid().ToString(),
+ UnixTime = _unixTimeNow
+ });
+ messages.Add(new Message
+ {
+ MessageId = Guid.NewGuid().ToString(),
+ SenderUsername = Guid.NewGuid().ToString(),
+ UnixTime = _unixTimeNow
+ });
+
+ var getMessagesServiceResult = new GetMessagesServiceResult
+ {
+ Messages = messages,
+ NextContinuationToken = _nextContinuationToken
+ };
+
+ _messageServiceMock.Setup(m => m.GetMessages(_conversationId, 10, OrderBy.DESC, null, 0))
+ .ReturnsAsync(getMessagesServiceResult);
+
+ string nextUri = $"/api/conversations/{_conversationId}/messages" +
+ "&limit=10" +
+ $"&continuationToken={_nextContinuationToken}" +
+ "&lastSeenConversationTime=0";
+
+ var response = await _httpClient.GetAsync($"/api/conversations/{_conversationId}/messages/");
+ var json = await response.Content.ReadAsStringAsync();
+ var receivedGetMessagesResponse = JsonConvert.DeserializeObject(json);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(messages, receivedGetMessagesResponse.Messages);
+ Assert.Equal(nextUri, receivedGetMessagesResponse.NextUri);
+ }
+
+ [Fact]
+ public async Task GetMessages_InvalidArguments()
+ {
+ _messageServiceMock.Setup(m => m.GetMessages(_conversationId, 10, OrderBy.DESC, null, 0))
+ .ThrowsAsync(new ArgumentException());
+
+ var response = await _httpClient.GetAsync($"/api/conversations/{_conversationId}/messages/");
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetMessages_ConversationDoesNotExist()
+ {
+ _messageServiceMock.Setup(m => m.GetMessages(_conversationId, 10, OrderBy.DESC, null, 0))
+ .ThrowsAsync(new ConversationDoesNotExistException(
+ $"A conversation partition with the conversationId {_conversationId} does not exist."));
+
+ var response = await _httpClient.GetAsync($"/api/conversations/{_conversationId}/messages/");
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostMessage_Success()
+ {
+ var sendMessageResponse = new SendMessageResponse
+ {
+ CreatedUnixTime = _unixTimeNow
+ };
+
+ _messageServiceMock.Setup(m => m.AddMessage(_conversationId, false, _sendMessageRequest))
+ .ReturnsAsync(sendMessageResponse);
+
+ var response = await _httpClient.PostAsJsonAsync(
+ $"/api/conversations/{_conversationId}/messages/", _sendMessageRequest);
+ var json = await response.Content.ReadAsStringAsync();
+ var receivedSendMessageResponse = JsonConvert.DeserializeObject(json);
+
+ Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+ Assert.Equal(sendMessageResponse, receivedSendMessageResponse);
+ }
+
+ [Fact]
+ public async Task PostMessage_InvalidArguments()
+ {
+ _messageServiceMock.Setup(m => m.AddMessage(_conversationId, false, _sendMessageRequest))
+ .ThrowsAsync(new ArgumentException());
+
+ var response = await _httpClient.PostAsJsonAsync(
+ $"/api/conversations/{_conversationId}/messages/", _sendMessageRequest);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostMessage_UserNotParticipant()
+ {
+ _messageServiceMock.Setup(m => m.AddMessage(_conversationId, false, _sendMessageRequest))
+ .ThrowsAsync(new UserNotParticipantException(
+ $"User {_username} is not a participant of conversation {_conversationId}."));
+
+ var response = await _httpClient.PostAsJsonAsync(
+ $"/api/conversations/{_conversationId}/messages/", _sendMessageRequest);
+
+ Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostMessage_ProfileNotFound()
+ {
+ _messageServiceMock.Setup(m => m.AddMessage(_conversationId, false, _sendMessageRequest))
+ .ThrowsAsync(new ProfileNotFoundException(
+ $"A profile with the username {_username} was not found."));
+
+ var response = await _httpClient.PostAsJsonAsync(
+ $"/api/conversations/{_conversationId}/messages/", _sendMessageRequest);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostMessage_ConversationDoesNotExist()
+ {
+ _messageServiceMock.Setup(m => m.AddMessage(_conversationId, false, _sendMessageRequest))
+ .ThrowsAsync(new ConversationDoesNotExistException(
+ $"A conversation partition with the conversationId {_conversationId} does not exist."));
+
+ var response = await _httpClient.PostAsJsonAsync(
+ $"/api/conversations/{_conversationId}/messages/", _sendMessageRequest);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostMessage_MessageExists()
+ {
+ _messageServiceMock.Setup(m => m.AddMessage(_conversationId, false, _sendMessageRequest))
+ .ThrowsAsync(new MessageExistsException($"A message with ID {_sendMessageRequest.MessageId} already exists."));
+
+ var response = await _httpClient.PostAsJsonAsync(
+ $"/api/conversations/{_conversationId}/messages/", _sendMessageRequest);
+
+ Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
+ }
+}
\ No newline at end of file
diff --git a/ChatService.Web.Tests/Controllers/ImagesControllerTests.cs b/ChatService.Web.Tests/Controllers/ImagesControllerTests.cs
new file mode 100644
index 0000000..5ae893a
--- /dev/null
+++ b/ChatService.Web.Tests/Controllers/ImagesControllerTests.cs
@@ -0,0 +1,125 @@
+using System.Net;
+using System.Net.Http.Headers;
+using ChatService.Web.Dtos;
+using ChatService.Web.Exceptions;
+using ChatService.Web.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Newtonsoft.Json;
+
+namespace ChatService.Web.Tests.Controllers;
+
+public class ImagesControllerTests : IClassFixture>
+{
+ private readonly Mock _imageServiceMock = new();
+ private readonly HttpClient _httpClient;
+
+ private static readonly Image _image = new("image/jpeg", new MemoryStream());
+
+ private readonly MultipartFormDataContent _content = new();
+
+ private readonly StreamContent _fileContent = new StreamContent(_image.Content)
+ {
+ Headers = { ContentType = new MediaTypeHeaderValue(_image.ContentType) }
+ };
+
+ private readonly string _imageId = Guid.NewGuid().ToString();
+
+ public ImagesControllerTests(WebApplicationFactory factory)
+ {
+ _httpClient = factory.WithWebHostBuilder(builder =>
+ {
+ builder.ConfigureTestServices(services => { services.AddSingleton(_imageServiceMock.Object); });
+ }).CreateClient();
+ }
+
+ [Fact]
+ public async Task UploadImage_Success()
+ {
+ var uploadImageResponse = new UploadImageResponse(_imageId);
+
+ _imageServiceMock.Setup(m => m.UploadImage(It.IsAny()))
+ .ReturnsAsync(new UploadImageServiceResult(_imageId));
+
+ _content.Add(_fileContent,"File", "image.jpeg");
+
+ var response = await _httpClient.PostAsync("api/Images/", _content);
+
+ Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+
+ Assert.Equal($"http://localhost/api/Images/{_imageId}", response.Headers.GetValues("Location").First());
+
+ var json = await response.Content.ReadAsStringAsync();
+ var receivedUploadImageResponse = JsonConvert.DeserializeObject(json);
+
+ Assert.Equal(uploadImageResponse, receivedUploadImageResponse);
+ }
+
+ [Fact]
+ public async Task UploadImage_MissingFile()
+ {
+ var response = await _httpClient.PostAsync("api/Images/", _content);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+
+ _imageServiceMock.Verify( m => m.UploadImage(It.IsAny()), Times.Never);
+ }
+
+ [Fact]
+ public async Task UploadImage_InvalidImageType()
+ {
+ _imageServiceMock.Setup(m => m.UploadImage(It.IsAny()))
+ .ThrowsAsync(new InvalidImageTypeException($"Invalid image type {_image.ContentType}."));
+
+ _content.Add(_fileContent,"File", "text/plain");
+
+ var response = await _httpClient.PostAsync("api/Images/", _content);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task DownloadImage_Success()
+ {
+ var fileContentResult = new FileContentResult(_image.Content.ToArray(), _image.ContentType);
+
+ _imageServiceMock.Setup(m => m.DownloadImage(_imageId))
+ .ReturnsAsync(fileContentResult);
+
+ var response = await _httpClient.GetAsync($"api/Images/{_imageId}");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.NotNull(response.Content.Headers.ContentType);
+
+ var contentType = response.Content.Headers.ContentType.ToString();
+ var content = await response.Content.ReadAsByteArrayAsync();
+
+ Assert.Equal(fileContentResult.FileContents, content);
+ Assert.Equal(fileContentResult.ContentType, contentType);
+ }
+
+ [Fact]
+ public async Task DownloadImage_NotFound()
+ {
+ _imageServiceMock.Setup(m => m.DownloadImage(_imageId))
+ .ThrowsAsync( new ImageNotFoundException($"An image with id {_imageId} was not found."));
+
+ var response = await _httpClient.GetAsync($"api/Images/{_imageId}");
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task DownloadImage_InvalidArgument()
+ {
+ _imageServiceMock.Setup(m => m.DownloadImage(_imageId))
+ .ThrowsAsync( new ArgumentException("Invalid imageId"));
+
+ var response = await _httpClient.GetAsync($"api/Images/{_imageId}");
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+}
\ No newline at end of file
diff --git a/ChatService.Web.Tests/Controllers/ProfilesControllerTests.cs b/ChatService.Web.Tests/Controllers/ProfilesControllerTests.cs
new file mode 100644
index 0000000..33f78a1
--- /dev/null
+++ b/ChatService.Web.Tests/Controllers/ProfilesControllerTests.cs
@@ -0,0 +1,154 @@
+using System.Net;
+using System.Net.Http.Json;
+using ChatService.Web.Dtos;
+using ChatService.Web.Exceptions;
+using ChatService.Web.Services;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Newtonsoft.Json;
+
+namespace ChatService.Web.Tests.Controllers;
+
+public class ProfilesControllerTests : IClassFixture>
+{
+ private readonly Mock _profileServiceMock = new();
+ private readonly HttpClient _httpClient;
+ private readonly Profile _profile = new Profile
+ {
+ Username = "foobar",
+ FirstName = "Foo",
+ LastName = "Bar",
+ ProfilePictureId = "123"
+ };
+
+ public ProfilesControllerTests(WebApplicationFactory factory)
+ {
+ _httpClient = factory.WithWebHostBuilder(builder =>
+ {
+ builder.ConfigureTestServices(services =>
+ {
+ services.AddSingleton(_profileServiceMock.Object);
+ });
+ }).CreateClient();
+ }
+
+ [Fact]
+ public async Task GetProfile_Success()
+ {
+ _profileServiceMock.Setup(m => m.GetProfile(_profile.Username))
+ .ReturnsAsync(_profile);
+
+ var response = await _httpClient.GetAsync($"api/Profiles/{_profile.Username}");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ var json = await response.Content.ReadAsStringAsync();
+ var receivedProfile = JsonConvert.DeserializeObject(json);
+ Assert.Equal(_profile, receivedProfile);
+ }
+
+ [Fact]
+ public async Task GetProfile_ProfileNotFound()
+ {
+ _profileServiceMock.Setup(m => m.GetProfile(_profile.Username))
+ .ThrowsAsync(new ProfileNotFoundException($"A profile with the username {_profile.Username} was not found."));
+
+ var response = await _httpClient.GetAsync($"api/Profiles/{_profile.Username}");
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+
+ var json = await response.Content.ReadAsStringAsync();
+ Assert.Equal($"A profile with the username {_profile.Username} was not found.", json);
+ }
+
+ [Fact]
+ public async Task PostProfile_Success()
+ {
+ var response = await _httpClient.PostAsJsonAsync("api/Profiles/", _profile);
+
+ Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+
+ Assert.Equal($"http://localhost/api/Profiles/{_profile.Username}",
+ response.Headers.GetValues("Location").First());
+
+ var json = await response.Content.ReadAsStringAsync();
+ var receivedProfile = JsonConvert.DeserializeObject(json);
+ Assert.Equal(_profile, receivedProfile);
+
+ _profileServiceMock.Verify(mock => mock.AddProfile(_profile), Times.Once);
+ }
+
+ [Fact]
+ public async Task PostProfile_UsernameTaken()
+ {
+ _profileServiceMock.Setup(m => m.AddProfile(_profile))
+ .ThrowsAsync(new UsernameTakenException($"The username {_profile.Username} is taken."));
+
+ var response = await _httpClient.PostAsJsonAsync("api/Profiles/", _profile);
+
+ Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
+ }
+
+ [Theory]
+ [InlineData(null, "Foo", "Bar", "123")]
+ [InlineData("", "Foo", "Bar", "123")]
+ [InlineData(" ", "Foo", "Bar", "123")]
+ [InlineData("foobar", null, "Bar", "123")]
+ [InlineData("foobar", "", "Bar", "123")]
+ [InlineData("foobar", " ", "Bar", "123")]
+ [InlineData("foobar", "Foo", null, "123")]
+ [InlineData("foobar", "Foo", "", "123")]
+ [InlineData("foobar", "Foo", " ", "123")]
+ [InlineData("foobar", "Foo", "Bar", null)]
+ [InlineData("foobar", "Foo", "Bar", "")]
+ [InlineData("foobar", "Foo", "Bar", " ")]
+ public async Task PostProfile_InvalidArguments(string username, string firstName, string lastName, string profilePictureId)
+ {
+ _profileServiceMock.Setup(m => m.AddProfile(_profile))
+ .ThrowsAsync(new ArgumentException($"Invalid profile {_profile}"));
+
+ Profile profile = new Profile
+ {
+ Username = username,
+ FirstName = firstName,
+ LastName = lastName,
+ ProfilePictureId = profilePictureId
+ };
+
+ var response = await _httpClient.PostAsJsonAsync("api/Profiles/", profile);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostProfile_InvalidUsername()
+ {
+ Profile profile = new Profile
+ {
+ Username = "username_with_underscore",
+ FirstName = "firstName",
+ LastName = "lastName",
+ ProfilePictureId = "profilePictureId"
+ };
+
+ _profileServiceMock.Setup(m => m.AddProfile(profile))
+ .ThrowsAsync(new InvalidUsernameException($"Username {profile.Username} is invalid. Usernames cannot have an underscore."));
+
+ var response = await _httpClient.PostAsJsonAsync("api/Profiles/", profile);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ProfileProfile_ProfilePictureNotFound()
+ {
+ _profileServiceMock.Setup(m => m.AddProfile(_profile))
+ .ThrowsAsync(new ImageNotFoundException("Invalid profile picture ID."));
+
+ var response = await _httpClient.PostAsJsonAsync("api/Profiles/", _profile);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+}
\ No newline at end of file
diff --git a/ChatService.Web.Tests/Services/ImageServiceTests.cs b/ChatService.Web.Tests/Services/ImageServiceTests.cs
new file mode 100644
index 0000000..4c14729
--- /dev/null
+++ b/ChatService.Web.Tests/Services/ImageServiceTests.cs
@@ -0,0 +1,87 @@
+using ChatService.Web.Dtos;
+using ChatService.Web.Exceptions;
+using ChatService.Web.Services;
+using ChatService.Web.Storage;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+
+namespace ChatService.Web.Tests.Services;
+
+public class ImageServiceTests : IClassFixture>
+{
+ private readonly Mock _imageStoreMock = new();
+ private readonly IImageService _imageService;
+
+ private readonly string _imageId = Guid.NewGuid().ToString();
+ private readonly Image _image = new("image/jpeg", new MemoryStream(new byte[] { 0x01, 0x02, 0x03 }));
+
+ public ImageServiceTests(WebApplicationFactory factory)
+ {
+ _imageService = factory.WithWebHostBuilder(builder =>
+ {
+ builder.ConfigureTestServices(services =>
+ {
+ services.AddSingleton(_imageStoreMock.Object);
+ });
+ }).Services.GetRequiredService();
+ }
+
+ [Fact]
+ public async Task UploadImage_Success()
+ {
+ _imageStoreMock.Setup(m => m.UploadImage(It.IsAny()))
+ .ReturnsAsync(_imageId);
+
+ var expectedUploadImageServiceResult = new UploadImageServiceResult(_imageId);
+
+ var receivedUploadImageServiceResult = await _imageService.UploadImage(_image);
+
+ Assert.Equal(expectedUploadImageServiceResult, receivedUploadImageServiceResult);
+ }
+
+ [Fact]
+ public async Task UploadImage_InvalidImageType()
+ {
+ var invalidImage = new Image("text/plain", new MemoryStream());
+
+ await Assert.ThrowsAsync(() => _imageService.UploadImage(invalidImage));
+
+ _imageStoreMock.Verify(m => m.UploadImage(It.IsAny()), Times.Never);
+ }
+
+ [Fact]
+ public async Task DownloadImage_Success()
+ {
+ var expectedFileContentResult = new FileContentResult(_image.Content.ToArray(), _image.ContentType);
+
+ _imageStoreMock.Setup(m => m.DownloadImage(_imageId))
+ .ReturnsAsync(_image);
+
+ var receivedFileContentResult = await _imageService.DownloadImage(_imageId);
+
+ Assert.Equal(expectedFileContentResult.ContentType, receivedFileContentResult.ContentType);
+ Assert.True(expectedFileContentResult.FileContents.SequenceEqual(receivedFileContentResult.FileContents));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public async Task DownloadImage_InvalidArguments(string imageId)
+ {
+
+ await Assert.ThrowsAsync(() => _imageService.DownloadImage(imageId));
+ }
+
+ [Fact]
+ public async Task DownloadImage_ImageNotFound()
+ {
+ _imageStoreMock.Setup(m => m.DownloadImage(_imageId))
+ .ReturnsAsync((Image?)null);
+
+ await Assert.ThrowsAsync(() => _imageService.DownloadImage(_imageId));
+ }
+}
\ No newline at end of file
diff --git a/ChatService.Web.Tests/Services/MessageServiceTests.cs b/ChatService.Web.Tests/Services/MessageServiceTests.cs
new file mode 100644
index 0000000..7a13f7d
--- /dev/null
+++ b/ChatService.Web.Tests/Services/MessageServiceTests.cs
@@ -0,0 +1,212 @@
+using ChatService.Web.Dtos;
+using ChatService.Web.Enums;
+using ChatService.Web.Exceptions;
+using ChatService.Web.Services;
+using ChatService.Web.Storage;
+using ChatService.Web.Utilities;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+
+namespace ChatService.Web.Tests.Services;
+
+public class MessageServiceTests : IClassFixture>
+{
+ private readonly Mock _messageStoreMock = new();
+ private readonly Mock _profileServiceMock = new();
+ private readonly IMessageService _messageService;
+
+ private static readonly string _senderUsername = Guid.NewGuid().ToString();
+
+ private static readonly string _recipientUsername = Guid.NewGuid().ToString();
+
+ private static readonly string _conversationId = ConversationIdUtilities.GenerateConversationId(_senderUsername, _recipientUsername);
+
+ private readonly long _unixTimeNow = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+
+ private readonly SendMessageRequest _sendMessageRequest = new SendMessageRequest
+ {
+ MessageId = Guid.NewGuid().ToString(),
+ SenderUsername = _senderUsername,
+ Text = "Hello"
+ };
+
+ public MessageServiceTests(WebApplicationFactory factory)
+ {
+ _messageService = factory.WithWebHostBuilder(builder =>
+ {
+ builder.ConfigureTestServices(services =>
+ {
+ services.AddSingleton(_messageStoreMock.Object);
+ services.AddSingleton(_profileServiceMock.Object);
+ });
+ }).Services.GetRequiredService();
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task AddMessage_Success(bool isFirstMessage)
+ {
+ _profileServiceMock.Setup(m => m.ProfileExists(_senderUsername))
+ .ReturnsAsync(true);
+
+ _messageStoreMock.Setup(m => m.ConversationPartitionExists(_conversationId))
+ .ReturnsAsync(true);
+
+ Message message = new Message
+ {
+ MessageId = _sendMessageRequest.MessageId,
+ UnixTime = _unixTimeNow,
+ SenderUsername = _sendMessageRequest.SenderUsername,
+ Text = _sendMessageRequest.Text
+ };
+
+ SendMessageResponse expectedSendMessageResponse = new SendMessageResponse
+ {
+ CreatedUnixTime = _unixTimeNow
+ };
+
+ SendMessageResponse receivedSendMessageResponse = await _messageService.AddMessage(
+ _conversationId, isFirstMessage, _sendMessageRequest);
+
+ _messageStoreMock.Verify(m => m.AddMessage(_conversationId, It.Is(
+ m => m.MessageId == message.MessageId
+ && m.SenderUsername == message.SenderUsername
+ && m.Text == message.Text)), Times.Once);
+
+ receivedSendMessageResponse.CreatedUnixTime = _unixTimeNow;
+
+ Assert.Equal(expectedSendMessageResponse, receivedSendMessageResponse);
+ }
+
+ [Theory]
+ [InlineData(null, "messageId", "senderUsername", "text")]
+ [InlineData("", "messageId", "senderUsername", "text")]
+ [InlineData(" ", "messageId", "senderUsername", "text")]
+ [InlineData("conversationId", null, "senderUsername", "text")]
+ [InlineData("conversationId", "", "senderUsername", "text")]
+ [InlineData("conversationId", " ", "senderUsername", "text")]
+ [InlineData("conversationId", "messageId", null, "text")]
+ [InlineData("conversationId", "messageId", "", "text")]
+ [InlineData("conversationId", "messageId", " ", "text")]
+ [InlineData("conversationId", "messageId", "senderUsername", null)]
+ [InlineData("conversationId", "messageId", "senderUsername", "")]
+ [InlineData("conversationId", "messageId", "senderUsername", " ")]
+ public async Task AddMessage_InvalidArguments(
+ string conversationId, string messageId, string senderUsername, string text)
+ {
+ SendMessageRequest sendMessageRequest = new SendMessageRequest
+ {
+ MessageId = messageId,
+ SenderUsername = senderUsername,
+ Text = text
+ };
+
+ await Assert.ThrowsAsync(() => _messageService.AddMessage(
+ conversationId, true, sendMessageRequest));
+ }
+
+ [Fact]
+ public async Task AddMessage_UserNotParticipant()
+ {
+ _sendMessageRequest.SenderUsername = Guid.NewGuid().ToString();
+
+ _messageStoreMock.Setup(m => m.ConversationPartitionExists(_conversationId))
+ .ReturnsAsync(true);
+
+ _profileServiceMock.Setup(m => m.ProfileExists(_sendMessageRequest.SenderUsername))
+ .ReturnsAsync(true);
+
+ await Assert.ThrowsAsync(
+ () => _messageService.AddMessage(_conversationId, true, _sendMessageRequest));
+ }
+
+ [Fact]
+ public async Task AddMessage_ProfileNotFound()
+ {
+ _profileServiceMock.Setup(m => m.ProfileExists(_senderUsername))
+ .ReturnsAsync(false);
+
+ await Assert.ThrowsAsync(() => _messageService.AddMessage(
+ _conversationId, true, _sendMessageRequest));
+ }
+
+ [Fact]
+ public async Task AddMessage_ConversationDoesNotExist()
+ {
+ _profileServiceMock.Setup(m => m.ProfileExists(_senderUsername))
+ .ReturnsAsync(true);
+
+ _messageStoreMock.Setup(m => m.ConversationPartitionExists(_conversationId))
+ .ReturnsAsync(false);
+
+ await Assert.ThrowsAsync(() => _messageService.AddMessage(
+ _conversationId, false, _sendMessageRequest));
+ }
+
+ [Fact]
+ public async Task GetMessages_Success()
+ {
+ _messageStoreMock.Setup(m => m.ConversationPartitionExists(_conversationId))
+ .ReturnsAsync(true);
+
+ List messages = new List {
+ new Message
+ {
+ MessageId = Guid.NewGuid().ToString(),
+ UnixTime = _unixTimeNow,
+ SenderUsername = _senderUsername,
+ Text = "Hello"
+ },
+ new Message
+ {
+ MessageId = Guid.NewGuid().ToString(),
+ UnixTime = _unixTimeNow,
+ SenderUsername = _senderUsername,
+ Text = "Good Bye"
+ }
+ };
+
+ string nextContinuationToken = Guid.NewGuid().ToString();
+
+ _messageStoreMock.Setup(m => m.GetMessages(_conversationId, 10, OrderBy.DESC, null, 0))
+ .ReturnsAsync((messages, nextContinuationToken));
+
+ GetMessagesServiceResult expectedGetMessagesServiceResult = new GetMessagesServiceResult
+ {
+ Messages = messages,
+ NextContinuationToken = nextContinuationToken
+ };
+
+ GetMessagesServiceResult receivedGetMessagesServiceResult = await _messageService.GetMessages(
+ _conversationId, 10, OrderBy.DESC, null, 0);
+
+ Assert.Equal(expectedGetMessagesServiceResult.Messages, receivedGetMessagesServiceResult.Messages);
+ Assert.Equal(expectedGetMessagesServiceResult.NextContinuationToken, receivedGetMessagesServiceResult.NextContinuationToken);
+ }
+
+ [Theory]
+ [InlineData(null, 1, 1)]
+ [InlineData("", 1, 1)]
+ [InlineData(" ", 1, 1)]
+ [InlineData("conversationId", 0, 1)]
+ [InlineData("conversationId", -1, 1)]
+ [InlineData("conversationId", 1, -1)]
+ public async Task GetMessages_InvalidArguments(string conversationId, int limit, long lastSeenConversationTime)
+ {
+ await Assert.ThrowsAsync(() => _messageService.GetMessages(conversationId, limit,
+ OrderBy.DESC, null, lastSeenConversationTime));
+ }
+
+ [Fact]
+ public async Task GetMessages_ConversationDoesNotExist()
+ {
+ _messageStoreMock.Setup(m => m.ConversationPartitionExists(_conversationId))
+ .ReturnsAsync(false);
+
+ await Assert.ThrowsAsync(() => _messageService.GetMessages(
+ _conversationId, 1, OrderBy.DESC, null, 0));
+ }
+}
\ No newline at end of file
diff --git a/ChatService.Web.Tests/Services/ProfileServiceTests.cs b/ChatService.Web.Tests/Services/ProfileServiceTests.cs
new file mode 100644
index 0000000..e7d589a
--- /dev/null
+++ b/ChatService.Web.Tests/Services/ProfileServiceTests.cs
@@ -0,0 +1,158 @@
+using ChatService.Web.Dtos;
+using ChatService.Web.Exceptions;
+using ChatService.Web.Services;
+using ChatService.Web.Storage;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+
+namespace ChatService.Web.Tests.Services;
+
+public class ProfileServiceTests : IClassFixture>
+{
+ private readonly Mock _profileStoreMock = new();
+ private readonly Mock _imageStoreMock = new();
+ private readonly IProfileService _profileService;
+
+ private readonly Profile _profile = new Profile
+ {
+ Username = "foobar",
+ FirstName = "Foo",
+ LastName = "Bar",
+ ProfilePictureId = "123"
+ };
+
+ public ProfileServiceTests(WebApplicationFactory factory)
+ {
+ _profileService = factory.WithWebHostBuilder(builder =>
+ {
+ builder.ConfigureTestServices(services =>
+ {
+ services.AddSingleton(_profileStoreMock.Object);
+ services.AddSingleton(_imageStoreMock.Object);
+ });
+ }).Services.GetRequiredService();
+ }
+
+ [Fact]
+ public async Task GetProfile_Success()
+ {
+ _profileStoreMock.Setup(m => m.GetProfile(_profile.Username))
+ .ReturnsAsync(_profile);
+
+ var receivedProfile = await _profileService.GetProfile(_profile.Username);
+
+ Assert.Equal(_profile, receivedProfile);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public async Task GetProfile_InvalidArguments(string username)
+ {
+ Assert.ThrowsAsync(() => _profileService.GetProfile(username));
+ }
+
+ [Fact]
+ public async Task AddNewProfile_Success()
+ {
+ _profileStoreMock.Setup(m => m.ProfileExists(_profile.Username))
+ .ReturnsAsync(false);
+ _imageStoreMock.Setup(m => m.ImageExists(_profile.ProfilePictureId))
+ .ReturnsAsync(true);
+
+ await _profileService.AddProfile(_profile);
+
+ _profileStoreMock.Verify(m => m.AddProfile(_profile), Times.Once);
+ }
+
+ [Fact]
+ public async Task AddNewProfile_NullProfile()
+ {
+ await Assert.ThrowsAsync( async () => await _profileService.AddProfile(null));
+ }
+
+ [Theory]
+ [InlineData(null, "Foo", "Bar", "dummy_id")]
+ [InlineData("", "Foo", "Bar", "dummy_id")]
+ [InlineData(" ", "Foo", "Bar", "dummy_id")]
+ [InlineData("foobar", null, "Bar", "dummy_id")]
+ [InlineData("foobar", "", "Bar", "dummy_id")]
+ [InlineData("foobar", " ", "Bar", "dummy_id")]
+ [InlineData("foobar", "Foo", null, "dummy_id")]
+ [InlineData("foobar", "Foo", "", "dummy_id")]
+ [InlineData("foobar", "Foo", " ", "dummy_id")]
+ [InlineData("foobar", "Foo", "Bar", null)]
+ [InlineData("foobar", "Foo", "Bar","")]
+ [InlineData("foobar", "Foo", "Bar"," ")]
+ public async Task AddNewProfile_InvalidArgs(string username, string firstName, string lastName, string profilePictureId)
+ {
+ Profile profile = new Profile
+ {
+ Username = username,
+ FirstName = firstName,
+ LastName = lastName,
+ ProfilePictureId = profilePictureId
+ };
+
+ await Assert.ThrowsAsync( async () => await _profileService.AddProfile(profile));
+ }
+
+ [Fact]
+ public async Task AddNewProfile_UsernameTaken()
+ {
+ _imageStoreMock.Setup(m => m.ImageExists(_profile.ProfilePictureId))
+ .ReturnsAsync(true);
+ _profileStoreMock.Setup(m => m.AddProfile(_profile))
+ .ThrowsAsync(new UsernameTakenException($"A profile with username {_profile.Username} already exists."));
+
+ await Assert.ThrowsAsync( async () => await _profileService.AddProfile(_profile));
+ }
+
+ [Fact]
+ public async Task AddNewProfile_InvalidUsername()
+ {
+ Profile profile = new Profile
+ {
+ Username = "username_with_underscore",
+ FirstName = "firstName",
+ LastName = "lastName",
+ ProfilePictureId = "profilePictureId"
+ };
+
+ await Assert.ThrowsAsync( async () => await _profileService.AddProfile(profile));
+ }
+
+ [Fact]
+ public async Task AddNewProfile_ProfilePictureNotFound()
+ {
+ _imageStoreMock.Setup(m => m.ImageExists(_profile.ProfilePictureId))
+ .ReturnsAsync(false);
+
+ await Assert.ThrowsAsync( async () => await _profileService.AddProfile(_profile));
+ }
+
+ [Fact]
+ public async Task DeleteProfile_Success()
+ {
+ _profileStoreMock.Setup(m => m.GetProfile(_profile.Username))
+ .ReturnsAsync(_profile);
+
+ await _profileService.DeleteProfile(_profile.Username);
+
+ _imageStoreMock.Verify(m => m.DeleteImage(_profile.ProfilePictureId), Times.Once);
+ _profileStoreMock.Verify(m => m.DeleteProfile(_profile.Username), Times.Once);
+ }
+
+ [Fact]
+ public async Task DeleteProfile_ProfileNotFound()
+ {
+ await Assert.ThrowsAsync(
+ async () => await _profileService.DeleteProfile(_profile.Username));
+
+ _imageStoreMock.Verify(m => m.DeleteImage(_profile.ProfilePictureId), Times.Never);
+ _profileStoreMock.Verify(m => m.DeleteProfile(_profile.Username), Times.Never);
+ }
+}
\ No newline at end of file
diff --git a/ChatService.Web.Tests/Services/UserConversationServiceTests.cs b/ChatService.Web.Tests/Services/UserConversationServiceTests.cs
new file mode 100644
index 0000000..77cae17
--- /dev/null
+++ b/ChatService.Web.Tests/Services/UserConversationServiceTests.cs
@@ -0,0 +1,268 @@
+using ChatService.Web.Dtos;
+using ChatService.Web.Enums;
+using ChatService.Web.Exceptions;
+using ChatService.Web.Services;
+using ChatService.Web.Storage;
+using ChatService.Web.Utilities;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+
+namespace ChatService.Web.Tests.Services;
+
+public class UserConversationServiceTests : IClassFixture>
+{
+ private readonly Mock _messageServiceMock = new();
+ private readonly Mock _userConversationStoreMock = new();
+ private readonly Mock _profileServiceMock = new();
+
+ private readonly IUserConversationService _userConversationService;
+
+ private static readonly long _unixTimeNow = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+
+ private static readonly List _participants = new List
+ {
+ Guid.NewGuid().ToString(),
+ Guid.NewGuid().ToString()
+ };
+
+ private static readonly SendMessageRequest _sendMessageRequest = new SendMessageRequest
+ {
+ MessageId = Guid.NewGuid().ToString(),
+ SenderUsername = _participants.ElementAt(0),
+ Text = "Hello World."
+ };
+
+ private readonly StartConversationRequest _startConversationRequest = new StartConversationRequest
+ {
+ Participants = _participants,
+ FirstMessage = _sendMessageRequest
+ };
+
+ public UserConversationServiceTests(WebApplicationFactory factory)
+ {
+ _userConversationService = factory.WithWebHostBuilder(builder =>
+ {
+ builder.ConfigureTestServices(services =>
+ {
+ services.AddSingleton(_messageServiceMock.Object);
+ services.AddSingleton(_userConversationStoreMock.Object);
+ services.AddSingleton(_profileServiceMock.Object);
+ });
+ }).Services.GetRequiredService();
+ }
+
+ [Fact]
+ public async Task CreateConversation_Success()
+ {
+ _profileServiceMock.Setup(m => m.ProfileExists(_participants.ElementAt(0)))
+ .ReturnsAsync(true);
+
+ _profileServiceMock.Setup(m => m.ProfileExists(_participants.ElementAt(1)))
+ .ReturnsAsync(true);
+
+ var response = await _userConversationService.CreateConversation(_startConversationRequest);
+
+ response.CreatedUnixTime = _unixTimeNow;
+
+ StartConversationServiceResult expected = new StartConversationServiceResult
+ {
+ ConversationId = ConversationIdUtilities.GenerateConversationId(
+ _participants.ElementAt(0), _participants.ElementAt(1)),
+ CreatedUnixTime = _unixTimeNow
+ };
+
+ Assert.Equal(expected, response);
+ }
+
+ [Theory]
+ [MemberData(nameof(GenerateInvalidParticipantsList))]
+ public async Task CreateConversation_InvalidParticipantsList(List participants)
+ {
+ StartConversationRequest startConversationRequest = new StartConversationRequest
+ {
+ Participants = participants,
+ FirstMessage = _sendMessageRequest
+ };
+
+ await Assert.ThrowsAsync( () =>
+ _userConversationService.CreateConversation(startConversationRequest));
+ }
+
+ [Theory]
+ [InlineData(null, "senderUsername", "Hello world.")]
+ [InlineData("", "senderUsername", "Hello world.")]
+ [InlineData(" ", "senderUsername", "Hello world.")]
+ [InlineData("messageId", null, "Hello world.")]
+ [InlineData("messageId", "", "Hello world.")]
+ [InlineData("messageId", " ", "Hello world.")]
+ [InlineData("messageId", "senderUsername", null)]
+ [InlineData("messageId", "senderUsername", "")]
+ [InlineData("messageId", "senderUsername", " ")]
+ public async Task CreateConversation_InvalidSendMessageRequest(string messageId, string senderUsername, string text)
+ {
+ SendMessageRequest sendMessageRequest = new SendMessageRequest
+ {
+ MessageId = messageId,
+ SenderUsername = senderUsername,
+ Text = text
+ };
+
+ _startConversationRequest.FirstMessage = sendMessageRequest;
+
+ await Assert.ThrowsAsync( () =>
+ _userConversationService.CreateConversation(_startConversationRequest));
+ }
+
+ [Fact]
+ public async Task CreateConversation_Profile1NotFound()
+ {
+ _profileServiceMock.Setup(m => m.ProfileExists(_participants.ElementAt(0)))
+ .ReturnsAsync(false);
+
+ _profileServiceMock.Setup(m => m.ProfileExists(_participants.ElementAt(1)))
+ .ReturnsAsync(true);
+
+ await Assert.ThrowsAsync( () =>
+ _userConversationService.CreateConversation(_startConversationRequest));
+ }
+
+ [Fact]
+ public async Task CreateConversation_Profile2NotFound()
+ {
+ _profileServiceMock.Setup(m => m.ProfileExists(_participants.ElementAt(0)))
+ .ReturnsAsync(true);
+
+ _profileServiceMock.Setup(m => m.ProfileExists(_participants.ElementAt(1)))
+ .ReturnsAsync(false);
+
+ await Assert.ThrowsAsync( () =>
+ _userConversationService.CreateConversation(_startConversationRequest));
+ }
+
+ [Fact]
+ public async Task GetUserConversations_Success()
+ {
+ string username1 = Guid.NewGuid().ToString();
+ string username2 = Guid.NewGuid().ToString();
+ string username3 = Guid.NewGuid().ToString();
+
+ _profileServiceMock.Setup(m => m.ProfileExists(username1))
+ .ReturnsAsync(true);
+
+ Profile profile2 = new Profile
+ {
+ Username = username2,
+ FirstName = Guid.NewGuid().ToString(),
+ LastName = Guid.NewGuid().ToString(),
+ ProfilePictureId = Guid.NewGuid().ToString()
+ };
+ Profile profile3 = new Profile
+ {
+ Username = username3,
+ FirstName = Guid.NewGuid().ToString(),
+ LastName = Guid.NewGuid().ToString(),
+ ProfilePictureId = Guid.NewGuid().ToString()
+ };
+
+ List userConversations = new List
+ {
+ new UserConversation
+ {
+ Username = username1,
+ ConversationId = ConversationIdUtilities.GenerateConversationId(username1, username2),
+ LastModifiedTime = _unixTimeNow
+ },
+ new UserConversation
+ {
+ Username = username1,
+ ConversationId = ConversationIdUtilities.GenerateConversationId(username1, username3),
+ LastModifiedTime = _unixTimeNow
+ }
+ };
+
+ string nextContinuationToken = Guid.NewGuid().ToString();
+
+ _userConversationStoreMock.Setup(m => m.GetUserConversations(
+ username1, 10, OrderBy.DESC, null, 0))
+ .ReturnsAsync((userConversations, nextContinuationToken));
+
+ _profileServiceMock.Setup(m => m.GetProfile(username2))
+ .ReturnsAsync(profile2);
+
+ _profileServiceMock.Setup(m => m.GetProfile(username3))
+ .ReturnsAsync(profile3);
+
+ List conversations = new List
+ {
+ new Conversation
+ {
+ ConversationId = ConversationIdUtilities.GenerateConversationId(username1, username2),
+ LastModifiedUnixTime = _unixTimeNow,
+ Recipient = profile2
+ },
+ new Conversation
+ {
+ ConversationId = ConversationIdUtilities.GenerateConversationId(username1, username3),
+ LastModifiedUnixTime = _unixTimeNow,
+ Recipient = profile3
+ }
+ };
+
+ GetUserConversationsServiceResult expected = new GetUserConversationsServiceResult
+ {
+ Conversations = conversations,
+ NextContinuationToken = nextContinuationToken
+ };
+
+ var response = await _userConversationService.GetUserConversations(
+ username1, 10, OrderBy.DESC, null, 0);
+
+ Assert.Equal(expected.Conversations, response.Conversations);
+ Assert.Equal(expected.NextContinuationToken, response.NextContinuationToken);
+ }
+
+ [Theory]
+ [InlineData(null, 10, OrderBy.DESC, null, 0)]
+ [InlineData("", 10, OrderBy.DESC, null, 0)]
+ [InlineData(" ", 10, OrderBy.DESC, null, 0)]
+ [InlineData("username", 0, OrderBy.DESC, null, 0)]
+ [InlineData("username", -1, OrderBy.DESC, null, 0)]
+ [InlineData("username", 10, OrderBy.DESC, null, -1)]
+ public async Task GetUserConversations_InvalidArguments(
+ string username, int limit, OrderBy orderBy, string? continuationToken, long lastSeenConversationTime)
+ {
+ await Assert.ThrowsAsync( () =>
+ _userConversationService.GetUserConversations(
+ username, limit, orderBy, continuationToken, lastSeenConversationTime));
+ }
+
+ [Fact]
+ public async Task GetUserConversations_UserNotFound()
+ {
+ _profileServiceMock.Setup(m => m.ProfileExists(_participants.ElementAt(0)))
+ .ReturnsAsync(false);
+
+ await Assert.ThrowsAsync( () =>
+ _userConversationService.GetUserConversations(
+ _participants.ElementAt(0), 10, OrderBy.DESC, null, 0));
+ }
+
+ public static IEnumerable