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..f590aee
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,456 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# Mac bundle stuff
+*.dmg
+*.app
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+##
+## Visual Studio Code
+##
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+ThirdPartyNotice.txt
\ 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/CosmosConversationStoreIntegrationTests.cs b/ChatService.Web.IntegrationTests/CosmosConversationStoreIntegrationTests.cs
new file mode 100644
index 0000000..7eb76f6
--- /dev/null
+++ b/ChatService.Web.IntegrationTests/CosmosConversationStoreIntegrationTests.cs
@@ -0,0 +1,270 @@
+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 CosmosConversationStoreIntegrationTests : IClassFixture>, IAsyncLifetime
+{
+ private readonly IUserConversationStore _userConversationStore;
+
+ private static readonly string _username = Guid.NewGuid().ToString();
+
+ private GetUserConversationsParameters _parameters = new()
+ {
+ Limit = 10,
+ Order = OrderBy.DESC,
+ ContinuationToken = null,
+ LastSeenConversationTime = 0
+ };
+
+ private static readonly UserConversation _userConversation = CreateUserConversation(
+ lastModifiedTime: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
+ private static readonly UserConversation _userConversation1 = CreateUserConversation(lastModifiedTime: 100);
+ private static readonly UserConversation _userConversation2 = CreateUserConversation(lastModifiedTime: 200);
+ private static readonly UserConversation _userConversation3 = CreateUserConversation(lastModifiedTime: 300);
+
+ private List _userConversations = new() {
+ _userConversation1, _userConversation2, _userConversation3, _userConversation
+ };
+
+ public CosmosConversationStoreIntegrationTests(WebApplicationFactory factory)
+ {
+ _userConversationStore = factory.Services.GetRequiredService();
+ }
+
+ [Fact]
+ public async Task CreateUserConversation_Success()
+ {
+ await _userConversationStore.UpsertUserConversation(_userConversation);
+ var receivedConversation = await _userConversationStore.GetUserConversation(
+ _userConversation.Username, _userConversation.ConversationId);
+
+ Assert.Equal(_userConversation, receivedConversation);
+ }
+
+ [Fact]
+ public async Task UpdateUserConversation_Success()
+ {
+ await _userConversationStore.UpsertUserConversation(_userConversation);
+ var receivedConversation = await _userConversationStore.GetUserConversation(
+ _userConversation.Username, _userConversation.ConversationId);
+
+ Assert.Equal(_userConversation, receivedConversation);
+
+ _userConversation.LastModifiedTime = 100;
+ await _userConversationStore.UpsertUserConversation(_userConversation);
+ receivedConversation = await _userConversationStore.GetUserConversation(
+ _userConversation.Username, _userConversation.ConversationId);
+
+ Assert.Equal(_userConversation, receivedConversation);
+ }
+
+ [Theory]
+ [InlineData(null, "dummyConversationId", 100)]
+ [InlineData("", "dummyConversationId", 100)]
+ [InlineData(" ", "dummyConversationId", 100)]
+ [InlineData("foobar", null, 100)]
+ [InlineData("foobar", "", 100)]
+ [InlineData("foobar", " ", 100)]
+ [InlineData("foobar", "dummyConversationId", -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.UpsertUserConversation(userConversation));
+ }
+
+ [Theory]
+ [InlineData(null, "dummyConversationId")]
+ [InlineData("", "dummyConversationId")]
+ [InlineData(" ", "dummyConversationId")]
+ [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()
+ {
+ Assert.Null(
+ await _userConversationStore.GetUserConversation(_userConversation.Username, _userConversation.ConversationId));
+ }
+
+ [Fact]
+ public async Task GetUserConversations_Limit()
+ {
+ await AddMultipleUserConversations(_userConversation1, _userConversation2, _userConversation3);
+
+ _parameters.Limit = 1;
+ var response = await _userConversationStore.GetUserConversations(_userConversation.Username, _parameters);
+ Assert.Single(response.UserConversations);
+
+ _parameters.Limit = 2;
+ response = await _userConversationStore.GetUserConversations(_userConversation.Username, _parameters);
+ Assert.Equal(2, response.UserConversations.Count);
+
+ _parameters.Limit = 3;
+ response = await _userConversationStore.GetUserConversations(_userConversation.Username, _parameters);
+ Assert.Equal(3, response.UserConversations.Count);
+ }
+
+ [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);
+
+ _parameters.Order = orderBy;
+ var response = await _userConversationStore.GetUserConversations(_userConversation.Username, _parameters);
+
+ if (orderBy == OrderBy.ASC)
+ {
+ Assert.Equal(userConversationsExpected, response.UserConversations);
+ }
+ else
+ {
+ userConversationsExpected.Reverse();
+ Assert.Equal(userConversationsExpected, response.UserConversations);
+ }
+ }
+
+ [Fact]
+ public async Task GetUserConversations_ContinuationTokenValidity()
+ {
+ await AddMultipleUserConversations(_userConversation1, _userConversation2, _userConversation3);
+
+ _parameters.Limit = 1;
+
+ var response = await _userConversationStore.GetUserConversations(_userConversation.Username, _parameters);
+
+ Assert.Equal(_userConversation3, response.UserConversations.ElementAt(0));
+
+ var nextContinuationToken = response.NextContinuationToken;
+ Assert.NotNull(nextContinuationToken);
+
+ _parameters.ContinuationToken = nextContinuationToken;
+ response = await _userConversationStore.GetUserConversations(_userConversation.Username, _parameters);
+ Assert.Equal(_userConversation2, response.UserConversations.ElementAt(0));
+
+ nextContinuationToken = response.NextContinuationToken;
+ Assert.NotNull(nextContinuationToken);
+
+ _parameters.ContinuationToken = nextContinuationToken;
+ response = await _userConversationStore.GetUserConversations(_userConversation.Username, _parameters);
+ Assert.Equal(_userConversation1, response.UserConversations.ElementAt(0));
+
+ nextContinuationToken = response.NextContinuationToken;
+ Assert.Null(nextContinuationToken);
+ }
+
+ [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();
+
+ foreach (UserConversation userConversation in _userConversations)
+ {
+ if (userConversation.LastModifiedTime > lastSeenConversationTime)
+ {
+ userConversationsExpected.Add(userConversation);
+ }
+ }
+ userConversationsExpected.Reverse();
+
+ _parameters.LastSeenConversationTime = lastSeenConversationTime;
+ var response = await _userConversationStore.GetUserConversations(_userConversation.Username, _parameters);
+
+ Assert.Equal(userConversationsExpected, response.UserConversations);
+ }
+
+ [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)
+ {
+ _parameters.Limit = limit;
+ _parameters.LastSeenConversationTime = lastSeenConversationTime;
+ await Assert.ThrowsAsync(
+ () => _userConversationStore.GetUserConversations(username, _parameters));
+ }
+
+ [Fact]
+ public async Task GetUserConversations_InvalidContinuationToken()
+ {
+ string invalidContinuationToken = Guid.NewGuid().ToString();
+ _parameters.ContinuationToken = invalidContinuationToken;
+
+ await Assert.ThrowsAsync(
+ () => _userConversationStore.GetUserConversations(_userConversation.Username, _parameters));
+ }
+
+ private async Task AddMultipleUserConversations(params UserConversation[] userConversations)
+ {
+ foreach (UserConversation userConversation in userConversations)
+ {
+ await _userConversationStore.UpsertUserConversation(userConversation);
+ }
+ }
+
+ private List CreateListOfUserConversations(params UserConversation[] userConversations)
+ {
+ List list = new();
+
+ foreach (UserConversation userConversation in userConversations)
+ {
+ list.Add(userConversation);
+ }
+
+ return list;
+ }
+
+ private static UserConversation CreateUserConversation(long lastModifiedTime)
+ {
+ return new UserConversation
+ {
+ Username = _username,
+ ConversationId = Guid.NewGuid().ToString(),
+ LastModifiedTime = lastModifiedTime
+ };
+ }
+
+ public Task InitializeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ public async Task DisposeAsync()
+ {
+ await Task.WhenAll(_userConversations.Select(
+ userConversation => _userConversationStore.DeleteUserConversation(
+ userConversation.Username, userConversation.ConversationId)));
+ }
+}
\ No newline at end of file
diff --git a/ChatService.Web.IntegrationTests/CosmosMessageStoreIntegrationTests.cs b/ChatService.Web.IntegrationTests/CosmosMessageStoreIntegrationTests.cs
new file mode 100644
index 0000000..6f1ed9b
--- /dev/null
+++ b/ChatService.Web.IntegrationTests/CosmosMessageStoreIntegrationTests.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 CosmosMessageStoreIntegrationTests : IClassFixture>, IAsyncLifetime
+{
+ private readonly IMessageStore _messageStore;
+
+ private readonly string _conversationId = Guid.NewGuid().ToString();
+
+ private GetMessagesParameters _parameters = new()
+ {
+ Limit = 1,
+ Order = OrderBy.ASC,
+ ContinuationToken = null,
+ LastSeenMessageTime = 0
+ };
+
+ private static readonly Message _message1 = CreateMessage(unixTime: 100, text: "text of _message1");
+ private static readonly Message _message2 = CreateMessage(unixTime: 200, text: "text of _message2");
+ private static readonly Message _message3 = CreateMessage(unixTime: 300, text: "text of _message3");
+
+ private List _messages = new() { _message1, _message2, _message3 };
+
+ public CosmosMessageStoreIntegrationTests(WebApplicationFactory factory)
+ {
+ _messageStore = factory.Services.GetRequiredService();
+ }
+
+ [Fact]
+ public async Task AddMessage_Success()
+ {
+ await _messageStore.AddMessage(_conversationId, _message1);
+ var receivedMessage = await _messageStore.GetMessage(_conversationId, _message1.Id);
+ Assert.Equal(_message1, receivedMessage);
+ }
+
+ [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()
+ {
+ Id = 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));
+ }
+
+ [Fact]
+ public async Task UpdateMessageTime_Success()
+ {
+ await _messageStore.AddMessage(_conversationId, _message1);
+ var receivedMessage = await _messageStore.GetMessage(_conversationId, _message1.Id);
+ Assert.Equal(_message1, receivedMessage);
+
+ _message1.UnixTime = 200;
+ await _messageStore.UpdateMessageTime(_conversationId, _message1);
+ receivedMessage = await _messageStore.GetMessage(_conversationId, _message1.Id);
+ Assert.Equal(_message1, receivedMessage);
+ }
+
+ [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()
+ {
+ var message = await _messageStore.GetMessage(_conversationId, _message1.Id);
+ Assert.Null(message);
+ }
+
+ [Fact]
+ public async Task GetMessages_Limit()
+ {
+ await AddMultipleMessages(_conversationId, _message1, _message2, _message3);
+
+ var response = await _messageStore.GetMessages(_conversationId, _parameters);
+ Assert.Single(response.Messages);
+
+ _parameters.Limit = 2;
+ response = await _messageStore.GetMessages(_conversationId, _parameters);
+ Assert.Equal(2, response.Messages.Count);
+
+ _parameters.Limit = 3;
+ response = await _messageStore.GetMessages(_conversationId, _parameters);
+ Assert.Equal(3, response.Messages.Count);
+ }
+
+ [Theory]
+ [InlineData(OrderBy.ASC)]
+ [InlineData(OrderBy.DESC)]
+ public async Task GetMessages_OrderBy(OrderBy orderBy)
+ {
+ await AddMultipleMessages(_conversationId, _message1, _message2);
+
+ List messagesExpected = new() { _message1, _message2 };
+
+ _parameters.Limit = 10;
+ _parameters.Order = orderBy;
+ var response = await _messageStore.GetMessages(_conversationId, _parameters);
+
+ if (orderBy == OrderBy.ASC)
+ {
+ Assert.Equal(messagesExpected, response.Messages);
+ }
+ else
+ {
+ messagesExpected.Reverse();
+ Assert.Equal(messagesExpected, response.Messages);
+ }
+ }
+
+ [Fact]
+ public async Task GetMessages_ContinuationTokenValidity()
+ {
+ await AddMultipleMessages(_conversationId, _message1, _message2, _message3);
+
+ var response = await _messageStore.GetMessages(_conversationId, _parameters);
+ Assert.Equal(_message1, response.Messages.ElementAt(0));
+ Assert.NotNull(response.NextContinuationToken);
+
+ _parameters.ContinuationToken = response.NextContinuationToken;
+ response = await _messageStore.GetMessages(_conversationId, _parameters);
+ Assert.Equal(_message2, response.Messages.ElementAt(0));
+ Assert.NotNull(response.NextContinuationToken);
+
+ _parameters.ContinuationToken = response.NextContinuationToken;
+ response = await _messageStore.GetMessages(_conversationId, _parameters);
+ Assert.Equal(_message3, response.Messages.ElementAt(0));
+ Assert.Null(response.NextContinuationToken);
+ }
+
+ [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();
+ foreach (Message message in _messages)
+ {
+ if (message.UnixTime > lastSeenMessageTime)
+ {
+ messagesExpected.Add(message);
+ }
+ }
+
+ _parameters.Limit = 10;
+ _parameters.LastSeenMessageTime = lastSeenMessageTime;
+ var response = await _messageStore.GetMessages(_conversationId, _parameters);
+
+ Assert.Equal(messagesExpected, response.Messages);
+ }
+
+ [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)
+ {
+ _parameters.Limit = limit;
+ _parameters.LastSeenMessageTime = lastSeenMessageTime;
+ await Assert.ThrowsAsync(() => _messageStore.GetMessages(conversationId, _parameters));
+ }
+
+ [Fact]
+ public async Task GetMessages_InvalidContinuationToken()
+ {
+ string invalidContinuationToken = Guid.NewGuid().ToString();
+ _parameters.Limit = 10;
+ _parameters.Order = OrderBy.DESC;
+ _parameters.ContinuationToken = invalidContinuationToken;
+ await Assert.ThrowsAsync(
+ () => _messageStore.GetMessages(_conversationId, _parameters));
+ }
+
+ [Fact]
+ public async Task ConversationPartitionExists_Exists()
+ {
+ await _messageStore.AddMessage(_conversationId, _message1);
+ Assert.True(await _messageStore.ConversationExists(_conversationId));
+ }
+
+ [Fact]
+ public async Task ConversationPartitionExists_DoesNotExists()
+ {
+ Assert.False(await _messageStore.ConversationExists(_conversationId));
+ }
+
+ private async Task AddMultipleMessages(string conversationId, params Message[] messages)
+ {
+ foreach (Message message in messages)
+ {
+ await _messageStore.AddMessage(conversationId, message);
+ }
+ }
+
+ private static Message CreateMessage(int unixTime, string text)
+ {
+ return new Message()
+ {
+ Id = Guid.NewGuid().ToString(),
+ UnixTime = unixTime,
+ SenderUsername = Guid.NewGuid().ToString(),
+ Text = text
+ };
+ }
+
+ public Task InitializeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ public async Task DisposeAsync()
+ {
+ await Task.WhenAll(_messages.Select(
+ message => _messageStore.DeleteMessage(_conversationId, message.Id)));
+ }
+}
\ No newline at end of file
diff --git a/ChatService.Web.IntegrationTests/CosmosProfileStoreIntegrationTests.cs b/ChatService.Web.IntegrationTests/CosmosProfileStoreIntegrationTests.cs
new file mode 100644
index 0000000..6ddc867
--- /dev/null
+++ b/ChatService.Web.IntegrationTests/CosmosProfileStoreIntegrationTests.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 CosmosProfileStoreIntegrationTests : IClassFixture>, IAsyncLifetime
+{
+ private readonly IProfileStore _profileStore;
+
+ private readonly Profile _profile = new()
+ {
+ 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 CosmosProfileStoreIntegrationTests(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()
+ {
+ 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/ImageStoreIntegrationTests.cs b/ChatService.Web.IntegrationTests/ImageStoreIntegrationTests.cs
new file mode 100644
index 0000000..3763d18
--- /dev/null
+++ b/ChatService.Web.IntegrationTests/ImageStoreIntegrationTests.cs
@@ -0,0 +1,87 @@
+using System.Text;
+using ChatService.Web.Dtos;
+using ChatService.Web.Storage;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ChatService.Web.IntegrationTests;
+
+public class ImageStoreIntegrationTests : IClassFixture>, IAsyncLifetime
+{
+ private readonly IImageStore _imageStore;
+ private readonly Image _image = new (
+ ContentType: "image/jpg",
+ Content: new MemoryStream(Encoding.UTF8.GetBytes("This is a mock image file content")));
+ private string _imageId;
+
+ public ImageStoreIntegrationTests(WebApplicationFactory factory)
+ {
+ _imageStore = factory.Services.GetRequiredService();
+ }
+
+ [Fact]
+ public async Task UploadDownloadImage_Success()
+ {
+ _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 UploadImage_WrongContentType()
+ // {
+ // var notImage = new Image(
+ // ContentType: "text/plain",
+ // Content: 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_NonExistingImage()
+ {
+ var downloadedImage = await _imageStore.DownloadImage("dummy_id");
+ Assert.Null(downloadedImage);
+ }
+
+ [Fact]
+ public async Task DeleteImage_Success()
+ {
+ _imageId = await _imageStore.UploadImage(_image);
+ var imageDeleted = await _imageStore.DeleteImage(_imageId);
+ var downloadedImage = await _imageStore.DownloadImage(_imageId);
+
+ Assert.True(imageDeleted);
+ Assert.Null(downloadedImage);
+ }
+
+ [Fact]
+ public async Task DeleteImage_NonExistingImage()
+ {
+ Assert.False(await _imageStore.DeleteImage("dummy_id"));
+ }
+
+ [Fact]
+ public async Task ImageExists_Exists()
+ {
+ _imageId = await _imageStore.UploadImage(_image);
+ Assert.True(await _imageStore.ImageExists(_imageId));
+ }
+
+ [Fact]
+ public async Task ImageExists_DoesntExist()
+ {
+ Assert.False(await _imageStore.ImageExists("dummy_id"));
+ }
+
+ public Task InitializeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _imageStore.DeleteImage(_imageId);
+ }
+}
\ 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..1fda9ce
--- /dev/null
+++ b/ChatService.Web.Tests/Controllers/ConversationsControllerTests.cs
@@ -0,0 +1,491 @@
+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 readonly string _conversationId = Guid.NewGuid().ToString();
+ private readonly static long _unixTimeNow = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ private readonly static string _nextContinuationToken = Guid.NewGuid().ToString();
+ private readonly string _nonUrlCharactersContinuationToken = "+\"#%&+/?:";
+
+ private GetMessagesParameters _getMessagesParameters = new()
+ {
+ Limit = 50,
+ Order = OrderBy.DESC,
+ ContinuationToken = null,
+ LastSeenMessageTime = 0
+ };
+
+ private GetUserConversationsParameters _getUserConversationsParameters = new()
+ {
+ Limit = 10,
+ Order = OrderBy.DESC,
+ ContinuationToken = null,
+ LastSeenConversationTime = 0
+ };
+
+ private static readonly SendMessageRequest _sendMessageRequest = new()
+ {
+ Id = Guid.NewGuid().ToString(),
+ SenderUsername = _username,
+ Text = "Hello"
+ };
+
+ private readonly StartConversationRequest _startConversationRequest = new()
+ {
+ Participants = new List { _username, Guid.NewGuid().ToString() },
+ FirstMessage = _sendMessageRequest
+ };
+
+ private readonly GetConversationsResult _getConversationsResult = new()
+ {
+ Conversations = new List{ CreateConversation(), CreateConversation() },
+ NextContinuationToken = _nextContinuationToken
+ };
+
+ 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()
+ {
+ _userConversationServiceMock.Setup(m => m.GetUserConversations(_username, _getUserConversationsParameters))
+ .ReturnsAsync(_getConversationsResult);
+
+ string nextUri = "/api/conversations" +
+ $"?username={_username}" +
+ $"&limit={_getUserConversationsParameters.Limit}" +
+ $"&lastSeenConversationTime={_getUserConversationsParameters.LastSeenConversationTime}" +
+ $"&continuationToken={WebUtility.UrlEncode(_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(_getConversationsResult.Conversations, receivedGetUserConversationsResponse.Conversations);
+ Assert.Equal(nextUri, receivedGetUserConversationsResponse.NextUri);
+ }
+
+ [Theory]
+ [InlineData(null, "1", "1")]
+ [InlineData("", "1", "1")]
+ [InlineData(" ", "1", "1")]
+ [InlineData("foobar", null, "1")]
+ [InlineData("foobar", "", "1")]
+ [InlineData("foobar", " ", "1")]
+ [InlineData("foobar", "NaN", "1")]
+ [InlineData("foobar", "1", null)]
+ [InlineData("foobar", "1", "")]
+ [InlineData("foobar", "1", " ")]
+ [InlineData("foobar", "1", "NaN")]
+ public async Task GetUserConversations_InvalidQueryParameters(string username, string limit, string lastSeenConversationTime)
+ {
+ var response = await _httpClient.GetAsync("/api/conversations" +
+ $"?username={username}" +
+ $"&limit={limit}" +
+ $"&lastSeenConversationTime={lastSeenConversationTime}");
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetUserConversations_ContinuationTokenEncode()
+ {
+ _getUserConversationsParameters.ContinuationToken = null;
+ _getConversationsResult.NextContinuationToken = _nonUrlCharactersContinuationToken;
+ _userConversationServiceMock.Setup(
+ m => m.GetUserConversations(_username, _getUserConversationsParameters))
+ .ReturnsAsync(_getConversationsResult);
+
+ var response = await _httpClient.GetAsync("/api/conversations" +
+ $"?username={_username}" +
+ $"&limit={_getUserConversationsParameters.Limit}" +
+ $"&lastSeenConversationTime={_getUserConversationsParameters.LastSeenConversationTime}" +
+ $"&continuationToken={_getUserConversationsParameters.ContinuationToken}");
+ var json = await response.Content.ReadAsStringAsync();
+ var receivedGetUserConversationsResponse = JsonConvert.DeserializeObject(json);
+
+ string expectedEncodedNextUri = "/api/conversations" +
+ $"?username={_username}" +
+ $"&limit={_getUserConversationsParameters.Limit}" +
+ $"&lastSeenConversationTime={_getUserConversationsParameters.LastSeenConversationTime}" +
+ $"&continuationToken={WebUtility.UrlEncode(_getConversationsResult.NextContinuationToken)}";
+
+ Assert.Equal(expectedEncodedNextUri, receivedGetUserConversationsResponse.NextUri);
+ }
+
+ [Fact]
+ public async Task GetUserConversations_ContinuationTokenDecode()
+ {
+ _getUserConversationsParameters.ContinuationToken = _nonUrlCharactersContinuationToken;
+ _userConversationServiceMock.Setup(
+ m => m.GetUserConversations(_username, _getUserConversationsParameters))
+ .ReturnsAsync(_getConversationsResult);
+
+ var response = await _httpClient.GetAsync("/api/conversations" +
+ $"?username={_username}" +
+ $"&limit={_getUserConversationsParameters.Limit}" +
+ $"&lastSeenConversationTime={_getUserConversationsParameters.LastSeenConversationTime}" +
+ $"&continuationToken={WebUtility.UrlEncode(_nonUrlCharactersContinuationToken)}");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ _userConversationServiceMock.Verify(
+ m => m.GetUserConversations(_username, _getUserConversationsParameters), Times.Once);
+ }
+
+ [Fact]
+ public async Task GetUserConversations_InvalidArguments()
+ {
+ _userConversationServiceMock.Setup(m => m.GetUserConversations(_username, _getUserConversationsParameters))
+ .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();
+ _getUserConversationsParameters.ContinuationToken = invalidContinuationToken;
+
+ _userConversationServiceMock.Setup(m => m.GetUserConversations(
+ _username, _getUserConversationsParameters))
+ .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, _getUserConversationsParameters))
+ .ThrowsAsync(new UserNotFoundException($"A user with the username {_username} was not found."));
+
+ var response = await _httpClient.GetAsync(
+ $"api/Conversations/?username={_username}&");
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetUserConversations_CosmosServiceUnavailable()
+ {
+ _userConversationServiceMock.Setup(m => m.GetUserConversations(
+ _username, _getUserConversationsParameters))
+ .ThrowsAsync(new CosmosServiceUnavailableException("Cosmos service is unavailable."));
+
+ var response = await _httpClient.GetAsync(
+ $"api/Conversations/?username={_username}&");
+
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task StartConversation_Success()
+ {
+ var startConversationServiceResult = new StartConversationResult
+ {
+ ConversationId = Guid.NewGuid().ToString(),
+ CreatedUnixTime = _unixTimeNow
+ };
+
+ _userConversationServiceMock.Setup(m => m.StartConversation(It.Is(
+ p => p.Participants.SequenceEqual(_startConversationRequest.Participants)
+ && p.FirstMessage == _startConversationRequest.FirstMessage)))
+ .ReturnsAsync(startConversationServiceResult);
+
+ var expectedStartConversationResponse = new StartConversationResponse
+ {
+ Id = 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.StartConversation(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.StartConversation(It.Is(
+ p => p.Participants.SequenceEqual(_startConversationRequest.Participants)
+ && p.FirstMessage == _startConversationRequest.FirstMessage)))
+ .ThrowsAsync(new UserNotFoundException($"A user 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.StartConversation(It.Is(
+ p => p.Participants.SequenceEqual(_startConversationRequest.Participants)
+ && p.FirstMessage == _startConversationRequest.FirstMessage)))
+ .ThrowsAsync(new MessageExistsException(
+ $"A message with ID {_startConversationRequest.FirstMessage.Id} already exists."));
+
+ var response = await _httpClient.PostAsJsonAsync($"api/Conversations/", _startConversationRequest);
+
+ Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task StartConversation_CosmosServiceUnavailable()
+ {
+ _userConversationServiceMock.Setup(m => m.StartConversation(It.Is(
+ p => p.Participants.SequenceEqual(_startConversationRequest.Participants)
+ && p.FirstMessage == _startConversationRequest.FirstMessage)))
+ .ThrowsAsync(new CosmosServiceUnavailableException("Cosmos service is unavailable."));
+
+ var response = await _httpClient.PostAsJsonAsync($"api/Conversations/", _startConversationRequest);
+
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetMessages_Success()
+ {
+ List messages = new();
+ messages.Add(new Message
+ {
+ Id = Guid.NewGuid().ToString(),
+ SenderUsername = Guid.NewGuid().ToString(),
+ UnixTime = _unixTimeNow
+ });
+ messages.Add(new Message
+ {
+ Id = Guid.NewGuid().ToString(),
+ SenderUsername = Guid.NewGuid().ToString(),
+ UnixTime = _unixTimeNow
+ });
+
+ var getMessagesServiceResult = new GetMessagesResult
+ {
+ Messages = messages,
+ NextContinuationToken = _nextContinuationToken
+ };
+
+ _messageServiceMock.Setup(m => m.GetMessages(_conversationId, _getMessagesParameters))
+ .ReturnsAsync(getMessagesServiceResult);
+
+ string nextUri = $"/api/conversations/{_conversationId}/messages" +
+ $"?limit={_getMessagesParameters.Limit}" +
+ $"&continuationToken={WebUtility.UrlEncode(_nextContinuationToken)}" +
+ $"&lastSeenConversationTime={_getUserConversationsParameters.LastSeenConversationTime}";
+
+ 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);
+ }
+
+ [Theory]
+ [InlineData(null, "1")]
+ [InlineData("", "1")]
+ [InlineData(" ", "1")]
+ [InlineData("Nan","1")]
+ [InlineData("1", null)]
+ [InlineData("1", "")]
+ [InlineData("1", " ")]
+ [InlineData("1", "NaN")]
+ public async Task GetMessages_InvalidQueryParameters(string limit, string lastSeenMessageTime)
+ {
+ var response = await _httpClient.GetAsync($"/api/conversations/{_conversationId}/messages/" +
+ $"?limit={limit}" +
+ $"&lastSeenMessageTime={lastSeenMessageTime}");
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetMessages_InvalidArguments()
+ {
+ _messageServiceMock.Setup(m => m.GetMessages(_conversationId, _getMessagesParameters))
+ .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, _getMessagesParameters))
+ .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 GetMessages_CosmosServiceUnavailable()
+ {
+ _messageServiceMock.Setup(m => m.GetMessages(_conversationId, _getMessagesParameters))
+ .ThrowsAsync(new CosmosServiceUnavailableException("Cosmos service is unavailable."));
+
+ var response = await _httpClient.GetAsync($"/api/conversations/{_conversationId}/messages/");
+
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, 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 UserNotFoundException(
+ $"A user 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.Id} already exists."));
+
+ var response = await _httpClient.PostAsJsonAsync(
+ $"/api/conversations/{_conversationId}/messages/", _sendMessageRequest);
+
+ Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostMessage_CosmosServiceUnavailable()
+ {
+ _messageServiceMock.Setup(m => m.AddMessage(_conversationId, false, _sendMessageRequest))
+ .ThrowsAsync(new CosmosServiceUnavailableException("Cosmos service is unavailable."));
+
+ var response = await _httpClient.PostAsJsonAsync(
+ $"/api/conversations/{_conversationId}/messages/", _sendMessageRequest);
+
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
+ }
+
+ private static Conversation CreateConversation()
+ {
+ return new Conversation
+ {
+ Id = Guid.NewGuid().ToString(),
+ LastModifiedUnixTime = _unixTimeNow
+ };
+ }
+}
\ 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..b5c397c
--- /dev/null
+++ b/ChatService.Web.Tests/Controllers/ImagesControllerTests.cs
@@ -0,0 +1,146 @@
+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(ContentType: "image/jpeg", Content: new MemoryStream());
+
+ private readonly MultipartFormDataContent _content = new();
+
+ private readonly StreamContent _fileContent = new(_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 UploadImageResult(_imageId));
+
+ _content.Add(_fileContent,"File", "image.jpeg");
+
+ var response = await _httpClient.PostAsync("api/Images/", _content);
+
+ Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+
+ 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 UploadImage_BlobServiceUnavailable()
+ {
+ _imageServiceMock.Setup(m => m.UploadImage(It.IsAny()))
+ .ThrowsAsync(new BlobServiceUnavailableException("Blob service is unavailable."));
+ _content.Add(_fileContent,"File", "image.jpeg");
+
+ var response = await _httpClient.PostAsync("api/Images/", _content);
+
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task DownloadImage_Success()
+ {
+ var fileContentResult = new FileContentResult(_image.Content.ToArray(), _image.ContentType);
+
+ _imageServiceMock.Setup(m => m.DownloadImage(_imageId))
+ .ReturnsAsync(_image);
+
+ 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);
+ }
+
+ [Fact]
+ public async Task DownloadImage_BlobServiceUnavailable()
+ {
+ _imageServiceMock.Setup(m => m.DownloadImage(_imageId))
+ .ThrowsAsync(new BlobServiceUnavailableException("Blob service is unavailable."));
+
+ var response = await _httpClient.GetAsync($"api/Images/{_imageId}");
+
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, 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..6cbe23a
--- /dev/null
+++ b/ChatService.Web.Tests/Controllers/ProfilesControllerTests.cs
@@ -0,0 +1,170 @@
+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()
+ {
+ 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/Profile/{_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 UserNotFoundException($"A user with the username {_profile.Username} was not found."));
+
+ var response = await _httpClient.GetAsync($"api/Profile/{_profile.Username}");
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetProfile_CosmosServiceUnavailable()
+ {
+ _profileServiceMock.Setup(m => m.GetProfile(_profile.Username))
+ .ThrowsAsync(new CosmosServiceUnavailableException("Cosmos service is unavailable."));
+
+ var response = await _httpClient.GetAsync($"api/Profile/{_profile.Username}");
+
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostProfile_Success()
+ {
+ var response = await _httpClient.PostAsJsonAsync("api/Profile/", _profile);
+
+ Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+
+ 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/Profile/", _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()
+ {
+ Username = username,
+ FirstName = firstName,
+ LastName = lastName,
+ ProfilePictureId = profilePictureId
+ };
+
+ var response = await _httpClient.PostAsJsonAsync("api/Profile/", profile);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostProfile_InvalidUsername()
+ {
+ Profile profile = new()
+ {
+ 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/Profile/", 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/Profile/", _profile);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ProfileProfile_CosmosServiceUnavailable()
+ {
+ _profileServiceMock.Setup(m => m.AddProfile(_profile))
+ .ThrowsAsync(new CosmosServiceUnavailableException("Cosmos service is unavailable."));
+
+ var response = await _httpClient.PostAsJsonAsync("api/Profile/", _profile);
+
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, 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..b2969ac
--- /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(
+ ContentType: "image/jpeg",
+ Content: 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 UploadImageResult(_imageId);
+
+ var receivedUploadImageServiceResult = await _imageService.UploadImage(_image);
+
+ Assert.Equal(expectedUploadImageServiceResult, receivedUploadImageServiceResult);
+ }
+
+ // [Fact]
+ // public async Task UploadImage_InvalidImageType()
+ // {
+ // var invalidImage = new Image(ContentType: "text/plain", Content: new MemoryStream());
+ //
+ // await Assert.ThrowsAsync(() => _imageService.UploadImage(invalidImage));
+ //
+ // _imageStoreMock.Verify(m => m.UploadImage(It.IsAny()), Times.Never);
+ // }
+
+ [Fact]
+ public async Task DownloadImage_Success()
+ {
+ _imageStoreMock.Setup(m => m.DownloadImage(_imageId))
+ .ReturnsAsync(_image);
+
+ var receivedImage = await _imageService.DownloadImage(_imageId);
+
+ Assert.Equal(_image.ContentType, receivedImage.ContentType);
+ Assert.True(_image.Content.ToArray().SequenceEqual(receivedImage.Content.ToArray()));
+ }
+
+ [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..935e33c
--- /dev/null
+++ b/ChatService.Web.Tests/Services/MessageServiceTests.cs
@@ -0,0 +1,259 @@
+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 _userConversationStoreMock = new();
+ private readonly Mock _profileServiceMock = new();
+ private readonly IMessageService _messageService;
+
+ private GetMessagesParameters _parameters = new()
+ {
+ Limit = 1,
+ Order = OrderBy.ASC,
+ ContinuationToken = null,
+ LastSeenMessageTime = 0
+ };
+
+ 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.ToUnixTimeMilliseconds();
+
+ private readonly SendMessageRequest _sendMessageRequest = new()
+ {
+ Id = Guid.NewGuid().ToString(),
+ SenderUsername = _senderUsername,
+ Text = Guid.NewGuid().ToString()
+ };
+
+ public MessageServiceTests(WebApplicationFactory factory)
+ {
+ _messageService = factory.WithWebHostBuilder(builder =>
+ {
+ builder.ConfigureTestServices(services =>
+ {
+ services.AddSingleton(_messageStoreMock.Object);
+ services.AddSingleton(_userConversationStoreMock.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.ConversationExists(_conversationId))
+ .ReturnsAsync(true);
+
+ Message message = new()
+ {
+ Id = _sendMessageRequest.Id,
+ UnixTime = _unixTimeNow,
+ SenderUsername = _sendMessageRequest.SenderUsername,
+ Text = _sendMessageRequest.Text
+ };
+
+ SendMessageResponse expectedSendMessageResponse = new()
+ {
+ CreatedUnixTime = _unixTimeNow
+ };
+
+ SendMessageResponse receivedSendMessageResponse = await _messageService.AddMessage(
+ _conversationId, isFirstMessage, _sendMessageRequest);
+
+ _messageStoreMock.Verify(m => m.AddMessage(_conversationId, It.Is(
+ m => m.Id == message.Id
+ && m.SenderUsername == message.SenderUsername
+ && m.Text == message.Text)), Times.Once);
+
+ List userConversations = CreateUserConversations(_conversationId, _unixTimeNow);
+ String username1 = userConversations.ElementAt(0).Username;
+ String username2 = userConversations.ElementAt(1).Username;
+
+ _userConversationStoreMock.Verify(m =>
+ m.UpsertUserConversation(It.Is(userConversation =>
+ userConversation.Username == username1 && userConversation.ConversationId == _conversationId)),
+ Times.Once);
+
+ _userConversationStoreMock.Verify(m =>
+ m.UpsertUserConversation(It.Is(userConversation =>
+ userConversation.Username == username2 && userConversation.ConversationId == _conversationId)),
+ 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()
+ {
+ Id = 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.ConversationExists(_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.ConversationExists(_conversationId))
+ .ReturnsAsync(false);
+
+ await Assert.ThrowsAsync(() => _messageService.AddMessage(
+ _conversationId, false, _sendMessageRequest));
+ }
+
+ [Fact]
+ public async Task GetMessages_Success()
+ {
+ _messageStoreMock.Setup(m => m.ConversationExists(_conversationId))
+ .ReturnsAsync(true);
+
+ List messages = new() { CreateMessage(), CreateMessage() };
+
+ string nextContinuationToken = Guid.NewGuid().ToString();
+
+ GetMessagesResult getMessagesResult = new()
+ {
+ Messages = messages,
+ NextContinuationToken = nextContinuationToken
+ };
+
+ _messageStoreMock.Setup(m => m.GetMessages(_conversationId, _parameters))
+ .ReturnsAsync(getMessagesResult);
+
+ GetMessagesResult expectedGetMessagesResult = new()
+ {
+ Messages = messages,
+ NextContinuationToken = nextContinuationToken
+ };
+
+ _parameters.Limit = 10;
+ _parameters.Order = OrderBy.DESC;
+ GetMessagesResult receivedGetMessagesResult = await _messageService.GetMessages(
+ _conversationId, _parameters);
+
+ Assert.Equal(expectedGetMessagesResult.Messages, receivedGetMessagesResult.Messages);
+ Assert.Equal(expectedGetMessagesResult.NextContinuationToken, receivedGetMessagesResult.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)
+ {
+ _parameters.Limit = limit;
+ _parameters.LastSeenMessageTime = lastSeenConversationTime;
+
+ await Assert.ThrowsAsync(() => _messageService.GetMessages(conversationId, _parameters));
+ }
+
+ [Fact]
+ public async Task GetMessages_ConversationDoesNotExist()
+ {
+ _messageStoreMock.Setup(m => m.ConversationExists(_conversationId))
+ .ReturnsAsync(false);
+
+ await Assert.ThrowsAsync(() => _messageService.GetMessages(
+ _conversationId, _parameters));
+ }
+
+ private Message CreateMessage()
+ {
+ return new Message()
+ {
+ Id = Guid.NewGuid().ToString(),
+ UnixTime = _unixTimeNow,
+ SenderUsername = _senderUsername,
+ Text = Guid.NewGuid().ToString()
+ };
+ }
+
+ private List CreateUserConversations(string conversationId, long unixTime)
+ {
+ string[] usernames = ConversationIdUtilities.SplitConversationId(conversationId);
+ List userConversations = new();
+
+ foreach (string username in usernames)
+ {
+ userConversations.Add(new UserConversation
+ {
+ ConversationId = conversationId,
+ Username = username,
+ LastModifiedTime = unixTime
+ });
+ }
+
+ return userConversations;
+ }
+}
\ 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..13771a3
--- /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 _imageServiceMock = new();
+ private readonly IProfileService _profileService;
+
+ private readonly Profile _profile = new()
+ {
+ Username = Guid.NewGuid().ToString(),
+ FirstName = Guid.NewGuid().ToString(),
+ LastName = Guid.NewGuid().ToString(),
+ ProfilePictureId = Guid.NewGuid().ToString()
+ };
+
+ public ProfileServiceTests(WebApplicationFactory factory)
+ {
+ _profileService = factory.WithWebHostBuilder(builder =>
+ {
+ builder.ConfigureTestServices(services =>
+ {
+ services.AddSingleton(_profileStoreMock.Object);
+ services.AddSingleton(_imageServiceMock.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)
+ {
+ await Assert.ThrowsAsync(() => _profileService.GetProfile(username));
+ }
+
+ [Fact]
+ public async Task AddNewProfile_Success()
+ {
+ _profileStoreMock.Setup(m => m.ProfileExists(_profile.Username))
+ .ReturnsAsync(false);
+ _imageServiceMock.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()
+ {
+ Username = username,
+ FirstName = firstName,
+ LastName = lastName,
+ ProfilePictureId = profilePictureId
+ };
+
+ await Assert.ThrowsAsync( async () => await _profileService.AddProfile(profile));
+ }
+
+ [Fact]
+ public async Task AddNewProfile_UsernameTaken()
+ {
+ _imageServiceMock.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()
+ {
+ Username = "username_with_underscore",
+ FirstName = "firstName",
+ LastName = "lastName",
+ ProfilePictureId = "profilePictureId"
+ };
+
+ await Assert.ThrowsAsync( async () => await _profileService.AddProfile(profile));
+ }
+
+ // [Fact]
+ // public async Task AddNewProfile_ProfilePictureNotFound()
+ // {
+ // _imageServiceMock.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);
+
+ _imageServiceMock.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));
+
+ _imageServiceMock.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..5886293
--- /dev/null
+++ b/ChatService.Web.Tests/Services/UserConversationServiceTests.cs
@@ -0,0 +1,274 @@
+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.ToUnixTimeMilliseconds();
+
+ private static readonly List _participants = new()
+ {
+ Guid.NewGuid().ToString(),
+ Guid.NewGuid().ToString()
+ };
+
+ private static readonly SendMessageRequest _sendMessageRequest = new()
+ {
+ Id = Guid.NewGuid().ToString(),
+ SenderUsername = _participants.ElementAt(0),
+ Text = Guid.NewGuid().ToString()
+ };
+
+ private GetUserConversationsParameters _parameters = new()
+ {
+ Limit = 1,
+ Order = OrderBy.ASC,
+ ContinuationToken = null,
+ LastSeenConversationTime = 0
+ };
+
+ private readonly StartConversationRequest _startConversationRequest = new()
+ {
+ 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.StartConversation(_startConversationRequest);
+
+ response.CreatedUnixTime = _unixTimeNow;
+
+ StartConversationResult expected = new()
+ {
+ 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()
+ {
+ Participants = participants,
+ FirstMessage = _sendMessageRequest
+ };
+ await Assert.ThrowsAsync( () =>
+ _userConversationService.StartConversation(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()
+ {
+ Id = messageId,
+ SenderUsername = senderUsername,
+ Text = text
+ };
+
+ _startConversationRequest.FirstMessage = sendMessageRequest;
+
+ await Assert.ThrowsAsync( () =>
+ _userConversationService.StartConversation(_startConversationRequest));
+ }
+
+ [Theory]
+ [InlineData(false, false)]
+ [InlineData(true, false)]
+ [InlineData(false, true)]
+ public async Task CreateConversation_ParticipantsNotFound(bool participant1Exists, bool participant2Exists)
+ {
+ _profileServiceMock.Setup(m => m.ProfileExists(_participants.ElementAt(0)))
+ .ReturnsAsync(participant1Exists);
+ _profileServiceMock.Setup(m => m.ProfileExists(_participants.ElementAt(1)))
+ .ReturnsAsync(participant2Exists);
+
+ await Assert.ThrowsAsync( () =>
+ _userConversationService.StartConversation(_startConversationRequest));
+ }
+
+ [Fact]
+ public async Task GetUserConversations_Success()
+ {
+ string username1 = Guid.NewGuid().ToString();
+ _profileServiceMock.Setup(m => m.ProfileExists(username1))
+ .ReturnsAsync(true);
+
+ string username2 = Guid.NewGuid().ToString();
+ string username3 = Guid.NewGuid().ToString();
+ Profile profile2 = CreateProfile(username2);
+ Profile profile3 = CreateProfile(username3);
+
+ List userConversations = new()
+ {
+ CreateUserConversation(senderUsername: username1, recipientUsername: username2),
+ CreateUserConversation(senderUsername: username1, recipientUsername: username3)
+ };
+
+ GetUserConversationsParameters parameters = new()
+ {
+ Limit = 10,
+ Order = OrderBy.DESC,
+ ContinuationToken = null,
+ LastSeenConversationTime = 0
+ };
+
+ string nextContinuationToken = Guid.NewGuid().ToString();
+ GetUserConversationsResult result = new()
+ {
+ UserConversations = userConversations,
+ NextContinuationToken = nextContinuationToken
+ };
+
+ _userConversationStoreMock.Setup(m => m.GetUserConversations(username1, parameters))
+ .ReturnsAsync(result);
+ _profileServiceMock.Setup(m => m.GetProfile(username2))
+ .ReturnsAsync(profile2);
+ _profileServiceMock.Setup(m => m.GetProfile(username3))
+ .ReturnsAsync(profile3);
+
+ List conversations = new()
+ {
+ CreateConversation(senderUsername: username1, recipientProfile: profile2),
+ CreateConversation(senderUsername: username1, recipientProfile: profile3)
+ };
+
+ GetConversationsResult expected = new()
+ {
+ Conversations = conversations,
+ NextContinuationToken = nextContinuationToken
+ };
+
+ _parameters.Limit = 10;
+ _parameters.Order = OrderBy.DESC;
+ var response = await _userConversationService.GetUserConversations(username1, _parameters);
+
+ 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)
+ {
+ _parameters.Limit = limit;
+ _parameters.Order = orderBy;
+ _parameters.ContinuationToken = continuationToken;
+ _parameters.LastSeenConversationTime = lastSeenConversationTime;
+ await Assert.ThrowsAsync( () =>
+ _userConversationService.GetUserConversations(username, _parameters));
+ }
+
+ [Fact]
+ public async Task GetUserConversations_UserNotFound()
+ {
+ _profileServiceMock.Setup(m => m.ProfileExists(_participants.ElementAt(0)))
+ .ReturnsAsync(false);
+
+ await Assert.ThrowsAsync( () =>
+ _userConversationService.GetUserConversations(_participants.ElementAt(0), _parameters));
+ }
+
+ public static IEnumerable