diff --git a/.gitignore b/.gitignore
index add57be..c39627f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,7 @@ bin/
obj/
/packages/
riderModule.iml
-/_ReSharper.Caches/
\ No newline at end of file
+/_ReSharper.Caches/
+
+
+do-not-commit.txt
\ No newline at end of file
diff --git a/.idea/.idea.SecureDesignProject/.idea/dataSources.xml b/.idea/.idea.SecureDesignProject/.idea/dataSources.xml
new file mode 100644
index 0000000..3d1eb39
--- /dev/null
+++ b/.idea/.idea.SecureDesignProject/.idea/dataSources.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ sqlserver.jb
+ true
+ com.jetbrains.jdbc.sqlserver.SqlServerDriver
+ Server=localhost,1433;Database=master
+
+
+
+
+
+
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.SecureDesignProject/.idea/sqldialects.xml b/.idea/.idea.SecureDesignProject/.idea/sqldialects.xml
new file mode 100644
index 0000000..1057460
--- /dev/null
+++ b/.idea/.idea.SecureDesignProject/.idea/sqldialects.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.SecureDesignProject/.idea/vcs.xml b/.idea/.idea.SecureDesignProject/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/.idea.SecureDesignProject/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Controllers/AuthController.cs b/Controllers/AuthController.cs
index c4d28aa..267a6cd 100644
--- a/Controllers/AuthController.cs
+++ b/Controllers/AuthController.cs
@@ -7,8 +7,17 @@
namespace SecureDesignProject.Controllers;
+///
+/// Controller responsible for handling authentication-related actions such as login, registration, and logout.
+///
public class AuthController(ILogger logger, AuthService authService) : Controller
{
+
+ ///
+ /// Displays the login page to the user.
+ ///
+ /// A ViewResult displaying the login page.
+
public IActionResult Login()
{
TempData["hideLogout"] = true;
@@ -16,6 +25,10 @@ public IActionResult Login()
return View();
}
+ ///
+ /// Displays the registration page to the user.
+ ///
+ /// A ViewResult displaying the registration page.
public IActionResult Register()
{
TempData["hideLogout"] = true;
@@ -23,6 +36,10 @@ public IActionResult Register()
return View();
}
+ ///
+ /// Logs out the user by invalidating their session and removing the session cookie.
+ ///
+ /// Redirects to the Login Page.
public IActionResult Logout()
{
var sessionCookie = Request.GetSessionCookie();
@@ -39,6 +56,13 @@ public IActionResult Logout()
return RedirectToAction("Login", "Auth");
}
+ ///
+ /// Handles the login request by validating credentials and setting a session cookie upon success.
+ ///
+ /// The login details submitted by the user (email and password).
+ ///
+ /// On success, redirects to the home page; otherwise, redisplays the login page with an error message.
+ ///
[HttpPost]
public IActionResult Login([FromForm] LoginDetails loginDetails)
{
@@ -57,6 +81,13 @@ public IActionResult Login([FromForm] LoginDetails loginDetails)
return RedirectToAction("Index", "Home");
}
+ ///
+ /// Handles the registration request by creating a new account and logging the user in upon success.
+ ///
+ /// The registration details submitted by the user.
+ ///
+ /// On success, redirects to the home page; otherwise, redisplays the registration page with an error message.
+ ///
[HttpPost]
public IActionResult Register([FromForm] RegisterDetails registerDetails)
{
@@ -75,6 +106,10 @@ public IActionResult Register([FromForm] RegisterDetails registerDetails)
return RedirectToAction("Index", "Home");
}
+ ///
+ /// Displays an error page for unhandled exceptions or issues.
+ ///
+ /// A ViewResult displaying the error page with error details.
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
diff --git a/Controllers/DashboardController.cs b/Controllers/DashboardController.cs
index d1d3a8f..6ef4032 100644
--- a/Controllers/DashboardController.cs
+++ b/Controllers/DashboardController.cs
@@ -5,8 +5,15 @@
namespace SecureDesignProject.Controllers;
+///
+/// Controller responsible for handling dashboard functionality for patients and caregivers.
+///
public class DashboardController(PatientService patientService, CaregiverService caregiverService) : Controller
{
+ ///
+ /// Displays patient-specific dashboard information.
+ ///
+ /// A ViewResult containing patient information or redirects to an error page if not found.
public IActionResult Patient()
{
var patient = patientService.GetPatientInfoByAccountId(HttpContext.GetAccountId());
@@ -17,6 +24,10 @@ public IActionResult Patient()
return View(patient);
}
+ ///
+ /// Displays caregiver-specific dashboard information.
+ ///
+ /// A ViewResult containing caregiver overview or redirects to an error page if not found.
public IActionResult Caregiver()
{
var caregiverOverview = caregiverService.GetCaregiverOverview(HttpContext.GetAccountId());
@@ -27,6 +38,11 @@ public IActionResult Caregiver()
return View(caregiverOverview);
}
+ ///
+ /// Displays patient information for a specific patient, intended for caregiver view.
+ ///
+ /// The unique identifier of the patient.
+ /// A ViewResult containing the patient's information.
[Route("dashboard/caregiverView/{patientId:guid}")]
public IActionResult CaregiverView([FromRoute] Guid patientId)
{
@@ -35,12 +51,23 @@ public IActionResult CaregiverView([FromRoute] Guid patientId)
return View(patientInfo);
}
+ ///
+ /// Displays the update address form for the logged-in patient.
+ ///
+ /// A ViewResult containing the patient's address.
public IActionResult UpdateAddress()
{
var address = patientService.GetAddressByAccountId(HttpContext.GetAccountId());
return View(address);
}
+ ///
+ /// Processes an update address request for the logged-in patient.
+ ///
+ /// The updated address information.
+ ///
+ /// Redirects to the patient dashboard if successful; otherwise, redisplays the form with an error message.
+ ///
[HttpPost]
public IActionResult UpdateAddress(Address address)
{
@@ -55,6 +82,11 @@ public IActionResult UpdateAddress(Address address)
return RedirectToAction("Patient");
}
+ ///
+ /// Displays the update appointment form for a specific appointment.
+ ///
+ /// The unique identifier of the appointment.
+ /// A ViewResult containing appointment information.
[Route("dashboard/updateAppointment/{appointmentId:guid}")]
public IActionResult UpdateAppointment([FromRoute] Guid appointmentId)
{
@@ -64,6 +96,11 @@ public IActionResult UpdateAppointment([FromRoute] Guid appointmentId)
}
+ ///
+ /// Creates a new appointment record and redirects to the update form.
+ ///
+ /// The unique identifier of the patient.
+ /// A RedirectToRouteResult pointing to the update appointment form.
[Route("dashboard/updateAppointment/new/{patientId:guid}")]
public IActionResult NewAppointment([FromRoute] Guid patientId)
{
@@ -72,6 +109,14 @@ public IActionResult NewAppointment([FromRoute] Guid patientId)
}
+ ///
+ /// Processes an update appointment request.
+ ///
+ /// The unique identifier of the appointment being updated.
+ /// The updated appointment information.
+ ///
+ /// Redirects to the caregiver dashboard if successful; otherwise, redisplays the form with an error message.
+ ///
[HttpPost]
[Route("dashboard/updateAppointment/{appointmentId:guid}")]
public IActionResult UpdateAppointment([FromRoute] Guid appointmentId, AppointmentInfo appointment)
@@ -87,6 +132,11 @@ public IActionResult UpdateAppointment([FromRoute] Guid appointmentId, Appointme
return RedirectToAction("Caregiver");
}
+ ///
+ /// Displays the update record form for a specific patient record.
+ ///
+ /// The unique identifier of the record.
+ /// A ViewResult containing the patient record.
[Route("dashboard/updateRecord/{recordId:guid}")]
public IActionResult UpdateRecord([FromRoute] Guid recordId)
{
@@ -96,6 +146,11 @@ public IActionResult UpdateRecord([FromRoute] Guid recordId)
}
+ ///
+ /// Creates a new patient record and redirects to the update form.
+ ///
+ /// The unique identifier of the patient.
+ /// A RedirectToRouteResult pointing to the update record form.
[Route("dashboard/updateRecord/new/{patientId:guid}")]
public IActionResult NewRecord([FromRoute] Guid patientId)
{
@@ -103,6 +158,14 @@ public IActionResult NewRecord([FromRoute] Guid patientId)
return RedirectToRoute($"Dashboard/UpdateAppointment/{newId}");
}
+ ///
+ /// Processes an update record request.
+ ///
+ /// The unique identifier of the record being updated.
+ /// The updated patient record information.
+ ///
+ /// Redirects to the caregiver dashboard if successful; otherwise, redisplays the form with an error message.
+ ///
[HttpPost]
[Route("dashboard/updateRecord/{recordId:guid}")]
public IActionResult UpdateRecord([FromRoute] Guid recordId, PatientRecord record)
diff --git a/Controllers/HomeController.cs b/Controllers/HomeController.cs
index 41184ab..81c9e18 100644
--- a/Controllers/HomeController.cs
+++ b/Controllers/HomeController.cs
@@ -6,9 +6,17 @@
namespace SecureDesignProject.Controllers;
+///
+/// The `HomeController` class handles navigation to the appropriate dashboard based on the user's account type.
+///
public class HomeController(ILogger logger, DatabaseService dbService) : Controller
{
- // Redirect to relevant dashboard depending on user type
+ ///
+ /// Determines the user's account type based on the session cookie and redirects to the appropriate dashboard.
+ ///
+ ///
+ /// A redirect to the patient's or caregiver's dashboard, or to the login page if the session is invalid.
+ ///
public IActionResult Index()
{
var sessionKey = Request.GetSessionCookie();
@@ -25,11 +33,11 @@ public IActionResult Index()
};
}
- public IActionResult Privacy()
- {
- return View();
- }
+ ///
+ /// Displays an error page when an exception or invalid state occurs.
+ ///
+ /// An `ErrorViewModel` with the current request's ID.
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
diff --git a/Extensions/HttpExtensions.cs b/Extensions/HttpExtensions.cs
index 04a20fb..02b5555 100644
--- a/Extensions/HttpExtensions.cs
+++ b/Extensions/HttpExtensions.cs
@@ -1,7 +1,18 @@
namespace SecureDesignProject.Extensions;
+
+///
+/// A static class providing extension methods for handling HTTP-related functionality
+/// such as cookies and retrieving account identifiers from the HTTP context.
+///
public static class HttpExtensions
{
+ ///
+ /// Sets a secure, HTTP-only cookie named "session_key" containing the session key as a hex string.
+ ///
+ /// The HTTP response object to set the cookie on.
+ /// The session key to be stored in the cookie.
+
public static void SetSessionKeyCookie(this HttpResponse response, byte[] sessionKey)
{
var options = new CookieOptions
@@ -13,12 +24,23 @@ public static void SetSessionKeyCookie(this HttpResponse response, byte[] sessio
}
+
+ ///
+ /// Retrieves the "session_key" cookie from the HTTP request, if it exists, and converts it to a byte array.
+ ///
+ /// The HTTP request object containing the cookies.
+ /// The session key as a byte array, or null if the cookie is not present.
public static byte[]? GetSessionCookie(this HttpRequest request)
{
var cookie = request.Cookies["session_key"];
return cookie == null ? null : Convert.FromHexString(cookie!);
}
+ ///
+ /// Retrieves the account ID stored in the HTTP context's items collection.
+ ///
+ /// The HTTP context object containing the account ID.
+ /// The account ID as a GUID, or Guid.Empty if not found.
public static Guid GetAccountId(this HttpContext context)
{
return context.Items["accountId"] as Guid? ?? Guid.Empty;
diff --git a/Extensions/StringExtensions.cs b/Extensions/StringExtensions.cs
index 9bba24b..7886ab5 100644
--- a/Extensions/StringExtensions.cs
+++ b/Extensions/StringExtensions.cs
@@ -1,5 +1,8 @@
namespace SecureDesignProject.Extensions;
+///
+/// A static class providing extension methods for string related functionality
+///
public static class StringExtensions
{
public static bool IsNullOrWhiteSpace(this string? value) => string.IsNullOrWhiteSpace(value);
diff --git a/Migrations/1_create_db.sql b/Migrations/1_create_db.sql
index d66078e..2927005 100644
--- a/Migrations/1_create_db.sql
+++ b/Migrations/1_create_db.sql
@@ -3,92 +3,94 @@
CREATE DATABASE HealthServiceDb
GO
+
-- Create Tables
CREATE TABLE Accounts(
- AccountId UNIQUEIDENTIFIER NOT NULL,
- Email NVARCHAR(MAX) NOT NULL UNIQUE,
- HashedPassword BINARY(256) NOT NULL,
-
- PRIMARY KEY (AccountId)
+ AccountId UNIQUEIDENTIFIER NOT NULL,
+ Email NVARCHAR(2048) NOT NULL UNIQUE,
+ HashedPassword BINARY(256) NOT NULL,
+
+ PRIMARY KEY (AccountId)
);
CREATE TABLE Patients(
- PatientId UNIQUEIDENTIFIER NOT NULL,
- FirstName NVARCHAR(255) NOT NULL,
- LastName NVARCHAR(255) NOT NULL,
- Address NVARCHAR(MAX),
-
- AccountId UNIQUEIDENTIFIER NOT NULL,
-
- PRIMARY KEY (PatientId),
- FOREIGN KEY (AccountId) REFERENCES Accounts(AccountId)
+ PatientId UNIQUEIDENTIFIER NOT NULL,
+ FirstName NVARCHAR(256) NOT NULL,
+ LastName NVARCHAR(256) NOT NULL,
+ Address NVARCHAR(2048),
+
+ AccountId UNIQUEIDENTIFIER NOT NULL,
+
+ PRIMARY KEY (PatientId),
+ FOREIGN KEY (AccountId) REFERENCES Accounts(AccountId)
);
CREATE TABLE Caregivers(
- CaregiverId UNIQUEIDENTIFIER NOT NULL,
- FirstName NVARCHAR(255) NOT NULL,
- LastName NVARCHAR(255) NOT NULL,
+ CaregiverId UNIQUEIDENTIFIER NOT NULL,
+ FirstName NVARCHAR(256) NOT NULL,
+ LastName NVARCHAR(256) NOT NULL,
- AccountId UNIQUEIDENTIFIER NOT NULL,
+ AccountId UNIQUEIDENTIFIER NOT NULL,
- PRIMARY KEY (CaregiverId),
- FOREIGN KEY (AccountId) REFERENCES Accounts(AccountId)
+ PRIMARY KEY (CaregiverId),
+ FOREIGN KEY (AccountId) REFERENCES Accounts(AccountId)
);
CREATE TABLE Caregiver_Patients(
- PatientId UNIQUEIDENTIFIER NOT NULL,
- CaregiverId UNIQUEIDENTIFIER NOT NULL,
-
- CurrentlyAssigned Bit NOT NULL,
- AssignedAt DATETIME NOT NULL,
- UnassignedAt DATETIME,
+ PatientId UNIQUEIDENTIFIER NOT NULL,
+ CaregiverId UNIQUEIDENTIFIER NOT NULL,
- FOREIGN KEY (PatientId) REFERENCES Patients(PatientId),
- FOREIGN KEY (CaregiverId) REFERENCES Caregivers(CaregiverId)
+ CurrentlyAssigned Bit NOT NULL,
+ AssignedAt DATETIME NOT NULL,
+ UnassignedAt DATETIME,
+
+ FOREIGN KEY (PatientId) REFERENCES Patients(PatientId),
+ FOREIGN KEY (CaregiverId) REFERENCES Caregivers(CaregiverId)
);
CREATE TABLE PatientsRecords(
RecordId UNIQUEIDENTIFIER NOT NULL,
-
+
PatientId UNIQUEIDENTIFIER NOT NULL,
CreatorId UNIQUEIDENTIFIER NOT NULL,
- RecordName NVARCHAR(MAX),
- RecordData NVARCHAR(MAX),
-
+ RecordName NVARCHAR(256),
+ RecordData NVARCHAR(2048),
+
PRIMARY KEY (RecordId),
FOREIGN KEY (PatientId) REFERENCES Patients(PatientId),
FOREIGN KEY (CreatorId) REFERENCES Caregivers(CaregiverId)
);
CREATE TABLE Appointments(
- AppointmentId UNIQUEIDENTIFIER NOT NULL,
+ AppointmentId UNIQUEIDENTIFIER NOT NULL,
- PatientId UNIQUEIDENTIFIER NOT NULL,
- CreatorId UNIQUEIDENTIFIER NOT NULL,
+ PatientId UNIQUEIDENTIFIER NOT NULL,
+ CreatorId UNIQUEIDENTIFIER NOT NULL,
- AppointmentTime DATETIME NOT NULL,
- Duration DATETIMEOFFSET NOT NULL,
- Notes NVARCHAR(MAX),
+ AppointmentTime DATETIME NOT NULL,
+ Duration DATETIMEOFFSET NOT NULL,
+ Notes NVARCHAR(2048),
- PRIMARY KEY (AppointmentId),
- FOREIGN KEY (PatientId) REFERENCES Patients(PatientId),
- FOREIGN KEY (CreatorId) REFERENCES Caregivers(CaregiverId)
+ PRIMARY KEY (AppointmentId),
+ FOREIGN KEY (PatientId) REFERENCES Patients(PatientId),
+ FOREIGN KEY (CreatorId) REFERENCES Caregivers(CaregiverId)
);
CREATE TABLE Sessions(
- SessionId UNIQUEIDENTIFIER NOT NULL,
- AccountId UNIQUEIDENTIFIER NOT NULL,
-
- SessionKey BINARY(256) NOT NULL,
- IsValid BIT NOT NULL,
-
- Created DATETIME NOT NULL,
- LastSeen DATETIME NOT NULL,
- Expires DATETIME NOT NULL,
-
- PRIMARY KEY (SessionId),
- FOREIGN KEY (AccountId) REFERENCES Accounts(AccountId)
+ SessionId UNIQUEIDENTIFIER NOT NULL,
+ AccountId UNIQUEIDENTIFIER NOT NULL,
+
+ SessionKey BINARY(256) NOT NULL,
+ IsValid BIT NOT NULL,
+
+ Created DATETIME NOT NULL,
+ LastSeen DATETIME NOT NULL,
+ Expires DATETIME NOT NULL,
+
+ PRIMARY KEY (SessionId),
+ FOREIGN KEY (AccountId) REFERENCES Accounts(AccountId)
);
GO
+
diff --git a/Migrations/2_add_seperate_salt_col.sql b/Migrations/2_add_seperate_salt_col.sql
index 7f4cb31..62ad2bb 100644
--- a/Migrations/2_add_seperate_salt_col.sql
+++ b/Migrations/2_add_seperate_salt_col.sql
@@ -1,2 +1,2 @@
ALTER TABLE Accounts
-ADD Salt BINARY(128) NOT NULL DEFAULT 123;
\ No newline at end of file
+ADD Salt BINARY(128) NOT NULL DEFAULT 0;
\ No newline at end of file
diff --git a/Models/AuthModels.cs b/Models/AuthModels.cs
index d311b76..553341c 100644
--- a/Models/AuthModels.cs
+++ b/Models/AuthModels.cs
@@ -31,9 +31,6 @@ public record RegisterDetails
[Required]
[MaxLength(255)]
public string LastName { get; init; } = "";
-
- [Required]
- public int InviteCode { get; init; }
};
public enum UserType
diff --git a/Program.cs b/Program.cs
index 349e466..3893a18 100644
--- a/Program.cs
+++ b/Program.cs
@@ -4,9 +4,11 @@
var builder = WebApplication.CreateBuilder(args);
+var connectionString = builder.Configuration.GetConnectionString("Database") ?? "";
+
// Add services to the container.
builder.Services.AddControllersWithViews();
-builder.Services.AddSingleton();
+builder.Services.AddSingleton(new DatabaseService(connectionString));
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
diff --git a/SecureDesignProject.csproj b/SecureDesignProject.csproj
index 22acc0c..9470c18 100644
--- a/SecureDesignProject.csproj
+++ b/SecureDesignProject.csproj
@@ -14,4 +14,16 @@
+
+
+ ..\..\.nuget\packages\moq\4.20.72\lib\net6.0\Moq.dll
+
+
+ ..\..\.nuget\packages\xunit.assert\2.9.2\lib\net6.0\xunit.assert.dll
+
+
+ ..\..\.nuget\packages\xunit.extensibility.core\2.9.2\lib\netstandard1.1\xunit.core.dll
+
+
+
diff --git a/SecureDesignProject.sln b/SecureDesignProject.sln
index 96e0ef7..8a52d05 100644
--- a/SecureDesignProject.sln
+++ b/SecureDesignProject.sln
@@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
compose.yaml = compose.yaml
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecureDesignProjectTests", "SecureDesignProjectTests\SecureDesignProjectTests.csproj", "{DCC98D37-7666-4880-9142-2103D87A6C35}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -17,5 +19,9 @@ Global
{46765F06-2B8D-47A4-A8D4-8961DA4BC3E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{46765F06-2B8D-47A4-A8D4-8961DA4BC3E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{46765F06-2B8D-47A4-A8D4-8961DA4BC3E9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DCC98D37-7666-4880-9142-2103D87A6C35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DCC98D37-7666-4880-9142-2103D87A6C35}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DCC98D37-7666-4880-9142-2103D87A6C35}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DCC98D37-7666-4880-9142-2103D87A6C35}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/SecureDesignProject.sln.DotSettings.user b/SecureDesignProject.sln.DotSettings.user
new file mode 100644
index 0000000..1b497e6
--- /dev/null
+++ b/SecureDesignProject.sln.DotSettings.user
@@ -0,0 +1,6 @@
+
+ ForceIncluded
+ ForceIncluded
+ ForceIncluded
+ ForceIncluded
+ ForceIncluded
\ No newline at end of file
diff --git a/SecureDesignProjectTests/ControllerTests/AuthControllerTests.cs b/SecureDesignProjectTests/ControllerTests/AuthControllerTests.cs
new file mode 100644
index 0000000..2162eea
--- /dev/null
+++ b/SecureDesignProjectTests/ControllerTests/AuthControllerTests.cs
@@ -0,0 +1,159 @@
+using System;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Moq;
+using SecureDesignProject.Controllers;
+using SecureDesignProject.Models;
+using SecureDesignProject.Services;
+using Xunit;
+
+public class AuthControllerTests
+{
+ private readonly Mock _authServiceMock;
+ private readonly Mock> _loggerMock;
+ private readonly AuthController _controller;
+
+ public AuthControllerTests()
+ {
+ _authServiceMock = new Mock();
+ _loggerMock = new Mock>();
+ _controller = new AuthController(_loggerMock.Object, _authServiceMock.Object);
+ }
+
+ [Fact]
+ public void Login_ShouldReturnViewResult()
+ {
+ // Act
+ var result = _controller.Login();
+
+ // Assert
+ var viewResult = Assert.IsType(result);
+ Assert.True((bool)_controller.TempData["hideLogout"]);
+ }
+
+ [Fact]
+ public void Register_ShouldReturnViewResult()
+ {
+ // Act
+ var result = _controller.Register();
+
+ // Assert
+ var viewResult = Assert.IsType(result);
+ Assert.True((bool)_controller.TempData["hideLogout"]);
+ }
+
+ [Fact]
+ public void Logout_ShouldRedirectToLogin_WhenSessionCookieIsNull()
+ {
+ // Arrange
+ var mockRequest = new Mock();
+ mockRequest.Setup(r => r.Cookies["session_key"]).Returns((string)null);
+ var mockContext = new Mock();
+ mockContext.Setup(c => c.Request).Returns(mockRequest.Object);
+
+ _controller.ControllerContext = new ControllerContext { HttpContext = mockContext.Object };
+
+ // Act
+ var result = _controller.Logout();
+
+ // Assert
+ var redirectResult = Assert.IsType(result);
+ Assert.Equal("Login", redirectResult.ActionName);
+ }
+
+ [Fact]
+ public void Logout_ShouldInvalidateSessionAndRedirectToLogin_WhenSessionCookieExists()
+ {
+ // Arrange
+ var sessionKey = new byte[] { 0x01, 0x02, 0x03 };
+ var mockRequest = new Mock();
+ var mockResponse = new Mock();
+ mockRequest.Setup(r => r.Cookies["session_key"]).Returns(Convert.ToHexString(sessionKey));
+ var mockContext = new Mock();
+ mockContext.Setup(c => c.Request).Returns(mockRequest.Object);
+ mockContext.Setup(c => c.Response).Returns(mockResponse.Object);
+
+ _controller.ControllerContext = new ControllerContext { HttpContext = mockContext.Object };
+
+ // Act
+ var result = _controller.Logout();
+
+ // Assert
+ _authServiceMock.Verify(s => s.InvalidateSession(sessionKey), Times.Once);
+ mockResponse.Verify(r => r.Cookies.Delete("session_key"), Times.Once);
+ var redirectResult = Assert.IsType(result);
+ Assert.Equal("Login", redirectResult.ActionName);
+ }
+
+ [Fact]
+ public void Login_ShouldSetCookieAndRedirectToHome_WhenLoginSucceeds()
+ {
+ // Arrange
+ var loginDetails = new LoginDetails { Email = "test@example.com", Password = "password" };
+ var sessionKey = new byte[] { 0x01, 0x02, 0x03 };
+ _authServiceMock.Setup(s => s.AttemptLogin(loginDetails))
+ .Returns((true, sessionKey));
+
+ var mockResponse = new Mock();
+ var mockContext = new Mock();
+ mockContext.Setup(c => c.Response).Returns(mockResponse.Object);
+
+ _controller.ControllerContext = new ControllerContext { HttpContext = mockContext.Object };
+
+ // Act
+ var result = _controller.Login(loginDetails);
+
+ // Assert
+ _authServiceMock.Verify(s => s.AttemptLogin(loginDetails), Times.Once);
+ mockResponse.Verify(
+ r => r.Cookies.Append("session_key", Convert.ToHexString(sessionKey), It.IsAny()),
+ Times.Once);
+ var redirectResult = Assert.IsType(result);
+ Assert.Equal("Index", redirectResult.ActionName);
+ }
+
+ [Fact]
+ public void Login_ShouldRedisplayViewWithError_WhenLoginFails()
+ {
+ // Arrange
+ var loginDetails = new LoginDetails { Email = "test@example.com", Password = "wrongpassword" };
+ _authServiceMock.Setup(s => s.AttemptLogin(loginDetails))
+ .Returns((false, null));
+
+ // Act
+ var result = _controller.Login(loginDetails);
+
+ // Assert
+ var viewResult = Assert.IsType(result);
+ Assert.True((bool)_controller.TempData["hideLogout"]);
+ }
+
+ [Fact]
+ public void Register_ShouldSetCookieAndRedirectToHome_WhenRegistrationSucceeds()
+ {
+ // Arrange
+ var registerDetails = new RegisterDetails { Email = "newuser@example.com", Password = "password" };
+ var sessionKey = new byte[] { 0x01, 0x02, 0x03 };
+ _authServiceMock.Setup(s => s.AttemptCreateAccount(registerDetails))
+ .Returns((true, sessionKey));
+
+ var mockResponse = new Mock();
+ var mockContext = new Mock();
+ mockContext.Setup(c => c.Response).Returns(mockResponse.Object);
+
+ _controller.ControllerContext = new ControllerContext { HttpContext = mockContext.Object };
+
+ // Act
+ var result = _controller.Register(registerDetails);
+
+ // Assert
+ _authServiceMock.Verify(s => s.AttemptCreateAccount(registerDetails), Times.Once);
+ mockResponse.Verify(
+ r => r.Cookies.Append("session_key", Convert.ToHexString(sessionKey), It.IsAny()),
+ Times.Once);
+ var redirectResult = Assert.IsType(result);
+ Assert.Equal("Index", redirectResult.ActionName);
+ }
+
+}
\ No newline at end of file
diff --git a/SecureDesignProjectTests/ControllerTests/DashboardController.cs b/SecureDesignProjectTests/ControllerTests/DashboardController.cs
new file mode 100644
index 0000000..cf43d9b
--- /dev/null
+++ b/SecureDesignProjectTests/ControllerTests/DashboardController.cs
@@ -0,0 +1,64 @@
+using System;
+using Microsoft.AspNetCore.Mvc;
+using Moq;
+using SecureDesignProject.Controllers;
+using SecureDesignProject.Models;
+using SecureDesignProject.Services;
+using Xunit;
+
+public class DashboardControllerTests
+{
+ private readonly Mock _patientServiceMock;
+ private readonly Mock _caregiverServiceMock;
+ private readonly DashboardController _controller;
+
+ public DashboardControllerTests()
+ {
+ _patientServiceMock = new Mock();
+ _caregiverServiceMock = new Mock();
+ _controller = new DashboardController(_patientServiceMock.Object, _caregiverServiceMock.Object);
+ }
+
+ [Fact]
+ public void Patient_ShouldRedirectToError_WhenPatientNotFound()
+ {
+ // Arrange
+ _patientServiceMock.Setup(s => s.GetPatientInfoByAccountId(It.IsAny())).Returns((PatientInfo)null);
+
+ // Act
+ var result = _controller.Patient();
+
+ // Assert
+ var redirectResult = Assert.IsType(result);
+ Assert.Equal("Error", redirectResult.ActionName);
+ }
+
+ [Fact]
+ public void Patient_ShouldReturnViewWithPatientInfo_WhenPatientFound()
+ {
+ // Arrange
+ var patientInfo = new PatientInfo {PatientName = "John Doe" };
+ _patientServiceMock.Setup(s => s.GetPatientInfoByAccountId(It.IsAny())).Returns(patientInfo);
+
+ // Act
+ var result = _controller.Patient();
+
+ // Assert
+ var viewResult = Assert.IsType(result);
+ Assert.Equal(patientInfo, viewResult.Model);
+ }
+
+ [Fact]
+ public void Caregiver_ShouldRedirectToError_WhenCaregiverOverviewNotFound()
+ {
+ // Arrange
+ _caregiverServiceMock.Setup(s => s.GetCaregiverOverview(It.IsAny())).Returns((CaregiverOverview)null);
+
+ // Act
+ var result = _controller.Caregiver();
+
+ // Assert
+ var redirectResult = Assert.IsType(result);
+ Assert.Equal("Error", redirectResult.ActionName);
+ }
+}
diff --git a/SecureDesignProjectTests/ControllerTests/HomeControllerTests.cs b/SecureDesignProjectTests/ControllerTests/HomeControllerTests.cs
new file mode 100644
index 0000000..6fbe0fb
--- /dev/null
+++ b/SecureDesignProjectTests/ControllerTests/HomeControllerTests.cs
@@ -0,0 +1,112 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Moq;
+using SecureDesignProject.Controllers;
+using SecureDesignProject.Models;
+using SecureDesignProject.Services;
+
+public class HomeControllerTests
+{
+ private readonly Mock _dbServiceMock;
+ private readonly Mock> _loggerMock;
+ private readonly HomeController _controller;
+
+ public HomeControllerTests()
+ {
+ _dbServiceMock = new Mock();
+ _loggerMock = new Mock>();
+ _controller = new HomeController(_loggerMock.Object, _dbServiceMock.Object);
+ }
+
+ [Fact]
+ public void Index_ShouldRedirectToLogin_WhenSessionKeyIsNull()
+ {
+ // Arrange
+ var httpContext = new DefaultHttpContext();
+ _controller.ControllerContext = new ControllerContext { HttpContext = httpContext };
+
+ // Act
+ var result = _controller.Index();
+
+ // Assert
+ var redirectResult = Assert.IsType(result);
+ Assert.Equal("Login", redirectResult.ActionName);
+ Assert.Equal("Auth", redirectResult.ControllerName);
+ }
+
+ [Fact]
+ public void Index_ShouldRedirectToPatientDashboard_WhenUserIsPatient()
+ {
+ // Arrange
+ var sessionKey = new byte[] { 1, 2, 3 };
+ var account = new Account { UserType = UserType.Patient };
+ var httpContext = new DefaultHttpContext();
+ _controller.ControllerContext = new ControllerContext { HttpContext = httpContext };
+
+ _dbServiceMock.Setup(db => db.GetAccountBySessionKey(sessionKey)).Returns(account);
+
+ // Act
+ var result = _controller.Index();
+
+ // Assert
+ var redirectResult = Assert.IsType(result);
+ Assert.Equal("Patient", redirectResult.ActionName);
+ Assert.Equal("Dashboard", redirectResult.ControllerName);
+ }
+
+ [Fact]
+ public void Index_ShouldRedirectToCaregiverDashboard_WhenUserIsCaregiver()
+ {
+ // Arrange
+ var sessionKey = new byte[] { 1, 2, 3 };
+ var account = new Account { UserType = UserType.Caregiver };
+ var httpContext = new DefaultHttpContext();
+ _controller.ControllerContext = new ControllerContext { HttpContext = httpContext };
+
+ _dbServiceMock.Setup(db => db.GetAccountBySessionKey(sessionKey)).Returns(account);
+
+ // Act
+ var result = _controller.Index();
+
+ // Assert
+ var redirectResult = Assert.IsType(result);
+ Assert.Equal("Caregiver", redirectResult.ActionName);
+ Assert.Equal("Dashboard", redirectResult.ControllerName);
+ }
+
+ [Fact]
+ public void Index_ShouldRedirectToLogin_WhenAccountIsInvalid()
+ {
+ // Arrange
+ var sessionKey = new byte[] { 1, 2, 3 };
+ var httpContext = new DefaultHttpContext();
+ _controller.ControllerContext = new ControllerContext { HttpContext = httpContext };
+
+ _dbServiceMock.Setup(db => db.GetAccountBySessionKey(sessionKey)).Returns((Account)null);
+
+ // Act
+ var result = _controller.Index();
+
+ // Assert
+ var redirectResult = Assert.IsType(result);
+ Assert.Equal("Login", redirectResult.ActionName);
+ Assert.Equal("Auth", redirectResult.ControllerName);
+ }
+
+ [Fact]
+ public void Error_ShouldReturnErrorViewModel()
+ {
+ // Arrange
+ var httpContext = new DefaultHttpContext();
+ _controller.ControllerContext = new ControllerContext { HttpContext = httpContext };
+
+ // Act
+ var result = _controller.Error();
+
+ // Assert
+ var viewResult = Assert.IsType(result);
+ var model = Assert.IsType(viewResult.Model);
+ Assert.Equal(httpContext.TraceIdentifier, model.RequestId);
+ }
+}
diff --git a/SecureDesignProjectTests/GlobalUsings.cs b/SecureDesignProjectTests/GlobalUsings.cs
new file mode 100644
index 0000000..8c927eb
--- /dev/null
+++ b/SecureDesignProjectTests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file
diff --git a/SecureDesignProjectTests/HttpExtensionTests.cs b/SecureDesignProjectTests/HttpExtensionTests.cs
new file mode 100644
index 0000000..20644da
--- /dev/null
+++ b/SecureDesignProjectTests/HttpExtensionTests.cs
@@ -0,0 +1,92 @@
+using System;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using SecureDesignProject.Extensions;
+using Xunit;
+
+public class HttpExtensionsTests
+{
+ [Fact]
+ public void SetSessionKeyCookie_ShouldSetCookieWithSessionKey()
+ {
+ // Arrange
+ var sessionKey = new byte[] { 0x01, 0x02, 0x03, 0x04 };
+ var mockResponse = new Mock();
+ var mockCookies = new Mock();
+ mockResponse.Setup(r => r.Cookies).Returns(mockCookies.Object);
+
+ // Act
+ mockResponse.Object.SetSessionKeyCookie(sessionKey);
+
+ // Assert
+ mockCookies.Verify(c => c.Append(
+ "session_key",
+ Convert.ToHexString(sessionKey),
+ It.Is(o => o.Expires > DateTime.Now && o.HttpOnly)), Times.Once);
+ }
+
+ [Fact]
+ public void GetSessionCookie_ShouldReturnSessionKey_WhenCookieExists()
+ {
+ // Arrange
+ var sessionKeyHex = "01020304";
+ var expectedSessionKey = Convert.FromHexString(sessionKeyHex);
+ var mockRequest = new Mock();
+ var mockCookies = new Mock();
+
+ mockCookies.Setup(c => c["session_key"]).Returns(sessionKeyHex);
+ mockRequest.Setup(r => r.Cookies).Returns(mockCookies.Object);
+
+ // Act
+ var result = mockRequest.Object.GetSessionCookie();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(expectedSessionKey, result);
+ }
+
+ [Fact]
+ public void GetSessionCookie_ShouldReturnNull_WhenCookieDoesNotExist()
+ {
+ // Arrange
+ var mockRequest = new Mock();
+ var mockCookies = new Mock();
+
+ mockCookies.Setup(c => c["session_key"]).Returns((string)null);
+ mockRequest.Setup(r => r.Cookies).Returns(mockCookies.Object);
+
+ // Act
+ var result = mockRequest.Object.GetSessionCookie();
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void GetAccountId_ShouldReturnAccountId_WhenContextHasAccountId()
+ {
+ // Arrange
+ var accountId = Guid.NewGuid();
+ var context = new DefaultHttpContext();
+ context.Items["accountId"] = accountId;
+
+ // Act
+ var result = context.GetAccountId();
+
+ // Assert
+ Assert.Equal(accountId, result);
+ }
+
+ [Fact]
+ public void GetAccountId_ShouldReturnEmptyGuid_WhenContextDoesNotHaveAccountId()
+ {
+ // Arrange
+ var context = new DefaultHttpContext();
+
+ // Act
+ var result = context.GetAccountId();
+
+ // Assert
+ Assert.Equal(Guid.Empty, result);
+ }
+}
diff --git a/SecureDesignProjectTests/SecureDesignProjectTests.csproj b/SecureDesignProjectTests/SecureDesignProjectTests.csproj
new file mode 100644
index 0000000..ec4800f
--- /dev/null
+++ b/SecureDesignProjectTests/SecureDesignProjectTests.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/SecureDesignProjectTests/ServiceTests/AuthServiceTests.cs b/SecureDesignProjectTests/ServiceTests/AuthServiceTests.cs
new file mode 100644
index 0000000..682f572
--- /dev/null
+++ b/SecureDesignProjectTests/ServiceTests/AuthServiceTests.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Linq;
+using System.Security.Cryptography;
+using Moq;
+using SecureDesignProject.Models;
+using SecureDesignProject.Services;
+using Xunit;
+
+namespace SecureDesignProject.Tests
+{
+ public class AuthServiceTests
+ {
+ private readonly Mock _mockDbService;
+ private readonly AuthService _authService;
+
+ public AuthServiceTests()
+ {
+ _mockDbService = new Mock();
+ _authService = new AuthService(_mockDbService.Object);
+ }
+
+
+ [Fact]
+ public void AttemptCreateAccount_WithValidDetails_ReturnsSuccess()
+ {
+ // Arrange
+ var email = "newuser@example.com";
+ var password = "Password123";
+ var registerDetails = new RegisterDetails
+ {
+ Email = email,
+ Password = password,
+ FirstName = "John",
+ LastName = "Doe"
+ };
+
+ _mockDbService.Setup(db => db.GetAccountByEmail(email)).Returns((Account)null);
+ _mockDbService.Setup(db => db.InsertAccount(It.IsAny())).Returns(new Account { AccountId = Guid.NewGuid() });
+ _mockDbService.Setup(db => db.InsertPatient(It.IsAny())).Returns(new Patient { PatientId = Guid.NewGuid() });
+
+ // Act
+ var result = _authService.AttemptCreateAccount(registerDetails);
+
+ // Assert
+ Assert.True(result.success);
+ Assert.NotEmpty(result.SessionKey);
+ }
+
+ [Fact]
+ public void ValidateSession_WithValidKey_ReturnsValid()
+ {
+ // Arrange
+ var sessionKey = RandomNumberGenerator.GetBytes(255);
+ var session = new Session
+ {
+ SessionId = Guid.NewGuid(),
+ AccountId = Guid.NewGuid(),
+ SessionKey = sessionKey,
+ IsValid = true
+ };
+
+ _mockDbService.Setup(db => db.GetSessionByKey(sessionKey)).Returns(session);
+
+ // Act
+ var result = _authService.ValidateSession(sessionKey);
+
+ // Assert
+ Assert.True(result.isValid);
+ Assert.Equal(session.AccountId, result.accountId);
+ }
+
+ [Fact]
+ public void InvalidateSession_WithValidKey_MarksSessionInvalid()
+ {
+ // Arrange
+ var sessionKey = RandomNumberGenerator.GetBytes(255);
+ var session = new Session
+ {
+ SessionId = Guid.NewGuid(),
+ AccountId = Guid.NewGuid(),
+ SessionKey = sessionKey,
+ IsValid = true,
+ Created = DateTime.Now,
+ LastSeen = DateTime.Now,
+ Expires = DateTime.Now.AddMinutes(10)
+ };
+
+ _mockDbService.Setup(db => db.GetSessionByKey(sessionKey)).Returns(session);
+ _mockDbService.Setup(db => db.UpdateSession(It.IsAny())).Verifiable();
+
+ // Act
+ _authService.InvalidateSession(sessionKey);
+
+ // Assert
+ _mockDbService.Verify(db => db.UpdateSession(It.Is(s => !s.IsValid)));
+ }
+ }
+}
diff --git a/SecureDesignProjectTests/ServiceTests/CaregiverServiceTests.cs b/SecureDesignProjectTests/ServiceTests/CaregiverServiceTests.cs
new file mode 100644
index 0000000..d4a5bba
--- /dev/null
+++ b/SecureDesignProjectTests/ServiceTests/CaregiverServiceTests.cs
@@ -0,0 +1,88 @@
+using Moq;
+using SecureDesignProject.Models;
+using SecureDesignProject.Services;
+
+public class CaregiverServiceTests
+{
+ private readonly Mock _mockDbService;
+ private readonly CaregiverService _caregiverService;
+
+ public CaregiverServiceTests()
+ {
+ _mockDbService = new Mock();
+ _caregiverService = new CaregiverService(_mockDbService.Object);
+ }
+
+ [Fact]
+ public void GetCaregiverOverview_CaregiverNotFound_ReturnsNull()
+ {
+ // Arrange
+ var accountId = Guid.NewGuid();
+ _mockDbService.Setup(db => db.GetCaregiverByAccountId(accountId)).Returns((Caregiver)null);
+
+ // Act
+ var result = _caregiverService.GetCaregiverOverview(accountId);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void GetCaregiverOverview_NoAssignedPatients_ReturnsOverviewWithEmptyPatients()
+ {
+ // Arrange
+ var accountId = Guid.NewGuid();
+ var caregiverId = Guid.NewGuid();
+ var caregiver = new Caregiver
+ {
+ CaregiverId = caregiverId,
+ FirstName = "John",
+ LastName = "Doe"
+ };
+
+ _mockDbService.Setup(db => db.GetCaregiverByAccountId(accountId)).Returns(caregiver);
+ _mockDbService.Setup(db => db.GetPatientsByAssignedCaregiver(caregiverId)).Returns(new List());
+
+ // Act
+ var result = _caregiverService.GetCaregiverOverview(accountId);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(caregiverId, result.CaregiverId);
+ Assert.Equal("John Doe", result.CaregiverName);
+ Assert.Empty(result.Patients);
+ }
+
+ [Fact]
+ public void GetCaregiverOverview_WithAssignedPatients_ReturnsOverviewWithPatients()
+ {
+ // Arrange
+ var accountId = Guid.NewGuid();
+ var caregiverId = Guid.NewGuid();
+ var caregiver = new Caregiver
+ {
+ CaregiverId = caregiverId,
+ FirstName = "Alice",
+ LastName = "Smith"
+ };
+ var patients = new List
+ {
+ new Patient { PatientId = Guid.NewGuid(), FirstName = "Patient1", LastName = "Test1" },
+ new Patient { PatientId = Guid.NewGuid(), FirstName = "Patient2", LastName = "Test2" }
+ };
+
+ _mockDbService.Setup(db => db.GetCaregiverByAccountId(accountId)).Returns(caregiver);
+ _mockDbService.Setup(db => db.GetPatientsByAssignedCaregiver(caregiverId)).Returns(patients);
+
+ // Act
+ var result = _caregiverService.GetCaregiverOverview(accountId);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(caregiverId, result.CaregiverId);
+ Assert.Equal("Alice Smith", result.CaregiverName);
+ Assert.Equal(2, result.Patients.Length);
+ Assert.Contains(result.Patients, p => p.Item1 == "Patient1 Test1" && p.Item2 == patients[0].PatientId);
+ Assert.Contains(result.Patients, p => p.Item1 == "Patient2 Test2" && p.Item2 == patients[1].PatientId);
+ }
+}
diff --git a/SecureDesignProjectTests/ServiceTests/PatientServiceTests.cs b/SecureDesignProjectTests/ServiceTests/PatientServiceTests.cs
new file mode 100644
index 0000000..64c7d7c
--- /dev/null
+++ b/SecureDesignProjectTests/ServiceTests/PatientServiceTests.cs
@@ -0,0 +1,155 @@
+using Moq;
+using SecureDesignProject.Models;
+using SecureDesignProject.Services;
+
+public class PatientServiceTests
+{
+ private readonly Mock _mockDbService;
+ private readonly PatientService _patientService;
+
+ public PatientServiceTests()
+ {
+ _mockDbService = new Mock();
+ _patientService = new PatientService(_mockDbService.Object);
+ }
+
+
+
+ [Fact]
+ public void GetAppointmentInfo_ShouldReturnNewAppointment_WhenAppointmentDoesNotExist()
+ {
+ // Arrange
+ Guid appointmentId = Guid.NewGuid();
+ _mockDbService.Setup(db => db.GetAppointmentById(appointmentId)).Returns((Appointment)null);
+
+ // Act
+ var result = _patientService.GetAppointmentInfo(appointmentId);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(appointmentId, result.AppointmentId);
+ Assert.Empty(result.Patient);
+ Assert.Empty(result.Caregiver);
+ Assert.Empty(result.AppointmentTime);
+ Assert.Empty(result.AppointmentDuration);
+ Assert.Empty(result.Notes);
+ }
+
+
+
+ [Fact]
+ public void AttemptUpdateAppointment_ShouldReturnFalse_WhenParsingFails()
+ {
+ // Arrange
+ var appointmentInfo = new AppointmentInfo
+ {
+ AppointmentTime = "Invalid Time",
+ AppointmentDuration = "Invalid Duration"
+ };
+
+ // Act
+ var result = _patientService.AttemptUpdateAppointment(appointmentInfo);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void AttemptUpdateAppointment_ShouldInsertNewAppointment_WhenAppointmentDoesNotExist()
+ {
+ // Arrange
+ var appointmentInfo = new AppointmentInfo
+ {
+ AppointmentId = Guid.NewGuid(),
+ AppointmentTime = DateTime.Now.ToString(),
+ AppointmentDuration = "60",
+ CreatorId = Guid.NewGuid(),
+ PatientId = Guid.NewGuid(),
+ Notes = "New Appointment"
+ };
+
+ _mockDbService.Setup(db => db.GetAppointmentById(appointmentInfo.AppointmentId ?? Guid.NewGuid())).Returns((Appointment)null);
+
+ // Act
+ var result = _patientService.AttemptUpdateAppointment(appointmentInfo);
+
+ // Assert
+ Assert.True(result);
+ _mockDbService.Verify(db => db.InsertAppointment(It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public void AttemptUpdateAppointment_ShouldUpdateExistingAppointment_WhenAppointmentExists()
+ {
+ // Arrange
+ var appointmentInfo = new AppointmentInfo
+ {
+ AppointmentId = Guid.NewGuid(),
+ AppointmentTime = DateTime.Now.ToString(),
+ AppointmentDuration = "60",
+ Notes = "Updated Appointment"
+ };
+
+ var existingAppointment = new Appointment
+ {
+ AppointmentId = appointmentInfo.AppointmentId ?? Guid.NewGuid(),
+ };
+
+ _mockDbService.Setup(db => db.GetAppointmentById(appointmentInfo.AppointmentId ?? Guid.NewGuid())).Returns(existingAppointment);
+
+ // Act
+ var result = _patientService.AttemptUpdateAppointment(appointmentInfo);
+
+ // Assert
+ Assert.True(result);
+ _mockDbService.Verify(db => db.UpdateAppointment(It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public void GetPatientInfoByAccountId_ShouldReturnNull_WhenPatientDoesNotExist()
+ {
+ // Arrange
+ Guid accountId = Guid.NewGuid();
+ _mockDbService.Setup(db => db.GetPatientByAccountId(accountId)).Returns((Patient)null);
+
+ // Act
+ var result = _patientService.GetPatientInfoByAccountId(accountId);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void AttemptUpdateAddress_ShouldReturnFalse_WhenPatientDoesNotExist()
+ {
+ // Arrange
+ Guid accountId = Guid.NewGuid();
+ var address = new Address();
+
+ _mockDbService.Setup(db => db.GetPatientByAccountId(accountId)).Returns((Patient)null);
+
+ // Act
+ var result = _patientService.AttemptUpdateAddress(accountId, address);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void AttemptUpdateAddress_ShouldUpdateAddress_WhenPatientExists()
+ {
+ // Arrange
+ Guid accountId = Guid.NewGuid();
+ var address = new Address { Line1 = "Line 1", Line2 = "Line 2", City = "City", Postcode = "Postcode" };
+ var patient = new Patient { PatientId = Guid.NewGuid(), Address = "Old Address" };
+
+ _mockDbService.Setup(db => db.GetPatientByAccountId(accountId)).Returns(patient);
+
+ // Act
+ var result = _patientService.AttemptUpdateAddress(accountId, address);
+
+ // Assert
+ Assert.True(result);
+ _mockDbService.Verify(db => db.UpdatePatient(It.IsAny()), Times.Once);
+ }
+}
diff --git a/Services/AuthService.cs b/Services/AuthService.cs
index 66de3cb..61f9e85 100644
--- a/Services/AuthService.cs
+++ b/Services/AuthService.cs
@@ -5,8 +5,18 @@
namespace SecureDesignProject.Services;
+///
+/// The AuthService class provides methods for handling user authentication,
+/// account creation, session management, and password hashing.
+///
public class AuthService(DatabaseService dbService)
{
+ ///
+ /// Attempts to log in a user by verifying their credentials.
+ /// If successful, generates and returns a session key.
+ ///
+ /// User login credentials, including email and password.
+ /// A tuple containing a success flag and the session key.
public (bool success, byte[] sessionKey) AttemptLogin(LoginDetails loginDetails)
{
var account = dbService.GetAccountByEmail(loginDetails.Email);
@@ -32,6 +42,11 @@ public class AuthService(DatabaseService dbService)
return (true, newSession.SessionKey);
}
+ ///
+ /// Validates a session using its key and returns the associated account ID if valid.
+ ///
+ /// The session key to validate.
+ /// A tuple containing a validity flag and the associated account ID.
public (bool isValid, Guid accountId) ValidateSession(byte[] key)
{
var session = dbService.GetSessionByKey(key);
@@ -39,6 +54,11 @@ public class AuthService(DatabaseService dbService)
return (session != null, session?.AccountId ?? Guid.Empty);
}
+ ///
+ /// Attempts to create a new user account. If successful, logs in the user automatically.
+ ///
+ /// Details for the new account, including email, password, and name.
+ /// A tuple containing a success flag and the new session key.
public (bool success, byte[] SessionKey) AttemptCreateAccount(RegisterDetails registerDetails)
{
var existingAccount = dbService.GetAccountByEmail(registerDetails.Email);
@@ -80,6 +100,10 @@ public class AuthService(DatabaseService dbService)
return (success, sessionKey);
}
+ ///
+ /// Invalidates an existing session by marking it as expired.
+ ///
+ /// The session key to invalidate.
public void InvalidateSession(byte[] key)
{
var session = dbService.GetSessionByKey(key);
@@ -94,6 +118,12 @@ public void InvalidateSession(byte[] key)
});
}
+ ///
+ /// Hashes a password using the Argon2id algorithm and a provided salt.
+ ///
+ /// The plaintext password to hash.
+ /// The salt to use for hashing.
+ /// The hashed password as a byte array.
private static byte[] HashPassword(string password, byte[] salt)
{
var argon2Id = new Argon2id(Encoding.UTF8.GetBytes(password));
@@ -107,11 +137,22 @@ private static byte[] HashPassword(string password, byte[] salt)
return hash;
}
+ ///
+ /// Generates a cryptographically secure random salt.
+ ///
+ /// A randomly generated salt as a byte array.
private static byte[] GenerateSalt()
{
return RandomNumberGenerator.GetBytes(128);
}
+ ///
+ /// Verifies if a plaintext password matches the stored hash using the provided salt.
+ ///
+ /// The plaintext password to verify.
+ /// The stored hashed password.
+ /// The salt used during hashing.
+ /// True if the password matches, false otherwise.
private static bool VerifyHash(string password, byte[] hash, byte[] salt)
{
var newHash = HashPassword(password, salt);
diff --git a/Services/CaregiverService.cs b/Services/CaregiverService.cs
index b05a809..7cd3df4 100644
--- a/Services/CaregiverService.cs
+++ b/Services/CaregiverService.cs
@@ -2,8 +2,22 @@
namespace SecureDesignProject.Services;
+
+///
+/// The CaregiverService class provides functionality related to caregivers,
+/// including retrieving an overview of their assigned patients.
+///
public class CaregiverService(DatabaseService dbService)
{
+
+ ///
+ /// Retrieves an overview of a caregiver, including their name and a list of assigned patients.
+ ///
+ /// The account ID associated with the caregiver.
+ ///
+ /// A object containing caregiver details and their assigned patients,
+ /// or null if no caregiver is associated with the provided account ID.
+ ///
public CaregiverOverview? GetCaregiverOverview(Guid accountId)
{
var caregiver = dbService.GetCaregiverByAccountId(accountId);
diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs
index c7087fd..e79e068 100644
--- a/Services/DatabaseService.cs
+++ b/Services/DatabaseService.cs
@@ -1,44 +1,43 @@
using System.Data.SqlClient;
using Dapper;
using SecureDesignProject.Models;
+using Record = SecureDesignProject.Models.Record;
namespace SecureDesignProject.Services;
-public class DatabaseService
+public class DatabaseService(string connectionString)
{
- private string ConnectionString = "Server=localhost,1433;Database=HealthServiceDb;User Id=SA;Password=Super-Secret-Password;TrustServerCertificate=true;Encrypt=false;";
-
public Account? GetAccountByEmail(string email)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
return db.QuerySingleOrDefault("SELECT * FROM Accounts WHERE Email = @email", new { email });
}
public Appointment? GetAppointmentById(Guid appointmentId)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
return db.QuerySingleOrDefault("SELECT * FROM Appointments WHERE AppointmentId = @appointmentId", new { appointmentId });
}
public Record? GetPatientRecordById(Guid recordId)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
return db.QuerySingleOrDefault("SELECT * FROM PatientsRecords WHERE RecordId = @recordId", new { recordId });
}
public Session? GetSessionByKey(byte[] key)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
return db.QuerySingleOrDefault("SELECT * FROM Sessions WHERE SessionKey = @key AND IsValid=1", new { key });
}
public Session? UpdateSession(Session session)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
db.Execute("""
UPDATE Sessions
@@ -52,7 +51,7 @@ UPDATE Sessions
public Patient? UpdatePatient(Patient patient)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
db.Execute("""
UPDATE Patients
@@ -66,7 +65,7 @@ UPDATE Patients
public Appointment UpdateAppointment(Appointment appointment)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
db.Execute("""
UPDATE Appointments
@@ -80,7 +79,7 @@ UPDATE Appointments
public Record UpdateRecord(Record record)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
db.Execute("""
UPDATE PatientsRecords
@@ -94,7 +93,7 @@ UPDATE PatientsRecords
public Account InsertAccount(Account account)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
db.Execute("insert into Accounts(AccountId, Email, HashedPassword, UserType, Salt) values (@AccountId, @Email, @HashedPassword, @UserType, @Salt)", account);
@@ -103,7 +102,7 @@ public Account InsertAccount(Account account)
public Appointment InsertAppointment(Appointment appointment)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
db.Execute("insert into Appointments(AppointmentId, PatientId, CreatorId, AppointmentTime, Duration, Notes) values (@AppointmentId, @PatientId, @CreatorId, @AppointmentTime, @Duration, @Notes)", appointment);
@@ -112,7 +111,7 @@ public Appointment InsertAppointment(Appointment appointment)
public Record InsertRecord(Record record)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
db.Execute("insert into PatientRecords(RecordId, PatientId, CreatorId, RecordName, RecordData) values (@recordId, @patientId, @creatorId, @recordName, @recordData)", record);
@@ -121,7 +120,7 @@ public Record InsertRecord(Record record)
public Patient InsertPatient(Patient patient)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
db.Execute("""
insert into Patients(PatientId, AccountId, FirstName, LastName, Address)
@@ -134,7 +133,7 @@ insert into Patients(PatientId, AccountId, FirstName, LastName, Address)
public Session InsertSession(Session session)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
db.Execute("""
insert into Sessions(SessionId, AccountId, SessionKey, IsValid, Created, LastSeen, Expires)
@@ -147,7 +146,7 @@ insert into Sessions(SessionId, AccountId, SessionKey, IsValid, Created, LastSee
public Account? GetAccountBySessionKey(byte[] sessionKey)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
return db.QuerySingleOrDefault("""
SELECT *
@@ -163,7 +162,7 @@ FROM Sessions
public Patient? GetPatientByAccountId(Guid accountId)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
return db.QuerySingleOrDefault("""
SELECT *
@@ -175,7 +174,7 @@ FROM Patients
public Patient? GetPatientByPatientId(Guid patientId)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
return db.QuerySingleOrDefault("""
SELECT *
@@ -186,7 +185,7 @@ FROM Patients
}
public Caregiver? GetCaregiverByAccountId(Guid accountId)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
return db.QuerySingleOrDefault("""
SELECT *
@@ -198,7 +197,7 @@ FROM Caregivers
public Caregiver? GetCaregiverById(Guid caregiverId)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
return db.QuerySingleOrDefault("""
SELECT *
@@ -210,7 +209,7 @@ FROM Caregivers
public IEnumerable GetCaregiversByAssignedPatient(Guid patientId)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
return db.Query("""
SELECT *
@@ -227,7 +226,7 @@ FROM Caregivers
public IEnumerable GetPatientsByAssignedCaregiver(Guid caregiverId)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
return db.Query("""
SELECT *
@@ -244,7 +243,7 @@ FROM Patients
public IEnumerable GetAppointmentsByPatient(Guid patientId)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
return db.Query("""
SELECT *
@@ -256,7 +255,7 @@ FROM Appointments
public IEnumerable GetRecordsByPatient(Guid patientId)
{
- using var db = new SqlConnection(ConnectionString);
+ using var db = new SqlConnection(connectionString);
return db.Query("""
SELECT *
diff --git a/Services/PatientService.cs b/Services/PatientService.cs
index 256d917..cd1ee6f 100644
--- a/Services/PatientService.cs
+++ b/Services/PatientService.cs
@@ -3,8 +3,20 @@
namespace SecureDesignProject.Services;
+///
+/// Provides services related to patient management, including fetching patient records,
+/// appointments, and updating patient information.
+///
public class PatientService(DatabaseService dbService)
{
+ ///
+ /// Retrieves a patient record by its ID or returns a new empty record if not found.
+ ///
+ /// The ID of the patient record, or null to indicate no specific ID.
+ ///
+ /// A object representing the patient's record.
+ /// If the record does not exist, a new empty record is returned.
+ ///
public PatientRecord GetPatientRecord(Guid? recordId)
{
var existingRecord = dbService.GetPatientRecordById(recordId ?? Guid.Empty);
@@ -34,6 +46,15 @@ public PatientRecord GetPatientRecord(Guid? recordId)
};
}
+
+ ///
+ /// Retrieves information about an appointment by its ID or returns a new empty appointment if not found.
+ ///
+ /// The ID of the appointment, or null to indicate no specific ID.
+ ///
+ /// An object containing the appointment details.
+ /// If the appointment does not exist, a new empty appointment is returned.
+ ///
public AppointmentInfo GetAppointmentInfo(Guid? appointmentId)
{
var appointment = dbService.GetAppointmentById(appointmentId ?? Guid.Empty);
@@ -65,6 +86,13 @@ public AppointmentInfo GetAppointmentInfo(Guid? appointmentId)
};
}
+ ///
+ /// Attempts to update an existing patient record with new data.
+ ///
+ /// The updated patient record.
+ ///
+ /// True if the record was successfully updated; otherwise, false if the record does not exist.
+ ///
public bool AttemptUpdatePatientRecord(PatientRecord patientRecord)
{
@@ -82,6 +110,13 @@ public bool AttemptUpdatePatientRecord(PatientRecord patientRecord)
return true;
}
+ ///
+ /// Attempts to update or create an appointment based on the provided information.
+ ///
+ /// The updated or new appointment information.
+ ///
+ /// True if the operation succeeds; otherwise, false if parsing of input data fails.
+ ///
public bool AttemptUpdateAppointment(AppointmentInfo appointmentInfo)
{
// parse string data
@@ -126,18 +161,36 @@ public bool AttemptUpdateAppointment(AppointmentInfo appointmentInfo)
return true;
}
+ ///
+ /// Retrieves detailed patient information by their account ID.
+ ///
+ /// The account ID of the patient.
+ ///
+ /// A object if the patient exists; otherwise, null.
+ ///
public PatientInfo? GetPatientInfoByAccountId(Guid accountId)
{
var patient = dbService.GetPatientByAccountId(accountId);
return patient == null ? null : GetPatientInfo(patient);
}
+
+ ///
+ /// Retrieves detailed patient information by their patient ID.
+ ///
+ /// The ID of the patient.
+ ///
+ /// A object if the patient exists; otherwise, null.
+ ///
public PatientInfo? GetPatientInfoByPatientId(Guid patientId)
{
var patient = dbService.GetPatientByPatientId(patientId);
return patient == null ? null : GetPatientInfo(patient);
}
+ ///
+ /// Retrieves detailed patient information for the given patient object.
+ ///
private PatientInfo GetPatientInfo(Patient patient)
{
var caregivers = dbService
@@ -205,6 +258,9 @@ private PatientInfo GetPatientInfo(Patient patient)
};
}
+ ///
+ /// Retrieves the address of a patient by their account ID.
+ ///
public Address? GetAddressByAccountId(Guid accountId)
{
var patient = dbService.GetPatientByAccountId(accountId);
@@ -224,6 +280,9 @@ private PatientInfo GetPatientInfo(Patient patient)
};
}
+ ///
+ /// Attempts to update the address of a patient by their account ID.
+ ///
public bool AttemptUpdateAddress(Guid accountId, Address address)
{
var patient = dbService.GetPatientByAccountId(accountId);
diff --git a/Views/Auth/Register.cshtml b/Views/Auth/Register.cshtml
index b35a623..87759fe 100644
--- a/Views/Auth/Register.cshtml
+++ b/Views/Auth/Register.cshtml
@@ -62,17 +62,6 @@
@class = "m-3 text-danger"
})
- @Html.TextBoxFor(m => m.InviteCode, new
- {
- id = "code-box",
- type="number",
- placeholder="Code",
- @class = "form-control m-3 w-auto"
- })
- @Html.ValidationMessageFor(model => model.InviteCode, default, new
- {
- @class = "m-3 text-danger"
- })
diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml
index 76698af..36ed910 100644
--- a/Views/Shared/_Layout.cshtml
+++ b/Views/Shared/_Layout.cshtml
@@ -22,9 +22,6 @@
Home
-
- Privacy
-
@@ -48,7 +45,7 @@