Refactor repository and SQL procedures; add repo tests

This commit is contained in:
Aaron Po
2026-01-25 23:26:40 -05:00
parent a56ea77861
commit 68ff549635
28 changed files with 573 additions and 430 deletions

View File

@@ -171,7 +171,6 @@ CREATE TABLE UserCredential -- delete credentials when user account is deleted
Timer ROWVERSION, Timer ROWVERSION,
CONSTRAINT PK_UserCredential CONSTRAINT PK_UserCredential
PRIMARY KEY (UserCredentialID), PRIMARY KEY (UserCredentialID),

View File

@@ -1,7 +1,7 @@
CREATE OR ALTER PROCEDURE usp_CreateUserAccount CREATE OR ALTER PROCEDURE usp_CreateUserAccount
( (
@UserAccountId UNIQUEIDENTIFIER = NULL, @UserAccountId UNIQUEIDENTIFIER OUTPUT,
@Username VARCHAR(64), @Username VARCHAR(64),
@FirstName NVARCHAR(128), @FirstName NVARCHAR(128),
@LastName NVARCHAR(128), @LastName NVARCHAR(128),
@@ -10,13 +10,10 @@ CREATE OR ALTER PROCEDURE usp_CreateUserAccount
) )
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON;
SET XACT_ABORT ON
BEGIN TRANSACTION
INSERT INTO UserAccount INSERT INTO UserAccount
( (
UserAccountID,
Username, Username,
FirstName, FirstName,
LastName, LastName,
@@ -25,12 +22,12 @@ BEGIN
) )
VALUES VALUES
( (
COALESCE(@UserAccountId, NEWID()),
@Username, @Username,
@FirstName, @FirstName,
@LastName, @LastName,
@DateOfBirth, @DateOfBirth,
@Email @Email
); );
COMMIT TRANSACTION
SELECT @UserAccountId AS UserAccountId;
END; END;

View File

@@ -6,8 +6,6 @@ CREATE OR ALTER PROCEDURE usp_DeleteUserAccount
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
SET XACT_ABORT ON
BEGIN TRANSACTION
IF NOT EXISTS (SELECT 1 FROM UserAccount WHERE UserAccountId = @UserAccountId) IF NOT EXISTS (SELECT 1 FROM UserAccount WHERE UserAccountId = @UserAccountId)
BEGIN BEGIN
@@ -19,5 +17,4 @@ BEGIN
DELETE FROM UserAccount DELETE FROM UserAccount
WHERE UserAccountId = @UserAccountId; WHERE UserAccountId = @UserAccountId;
COMMIT TRANSACTION
END; END;

View File

@@ -1,4 +1,3 @@
CREATE OR ALTER PROCEDURE usp_GetAllUserAccounts CREATE OR ALTER PROCEDURE usp_GetAllUserAccounts
AS AS
BEGIN BEGIN

View File

@@ -1,6 +1,4 @@
CREATE OR ALTER PROCEDURE usp_GetUserAccountByEmail(
CREATE OR ALTER PROCEDURE usp_GetUserAccountByEmail
(
@Email VARCHAR(128) @Email VARCHAR(128)
) )
AS AS

View File

@@ -1,6 +1,4 @@
CREATE OR ALTER PROCEDURE USP_GetUserAccountById(
CREATE OR ALTER PROCEDURE usp_GetUserAccountById
(
@UserAccountId UNIQUEIDENTIFIER @UserAccountId UNIQUEIDENTIFIER
) )
AS AS
@@ -18,4 +16,4 @@ BEGIN
Timer Timer
FROM dbo.UserAccount FROM dbo.UserAccount
WHERE UserAccountID = @UserAccountId; WHERE UserAccountID = @UserAccountId;
END; END

View File

@@ -1,6 +1,4 @@
CREATE OR ALTER PROCEDURE usp_GetUserAccountByUsername(
CREATE OR ALTER PROCEDURE usp_GetUserAccountByUsername
(
@Username VARCHAR(64) @Username VARCHAR(64)
) )
AS AS

View File

@@ -1,6 +1,4 @@
CREATE OR ALTER PROCEDURE usp_UpdateUserAccount(
CREATE OR ALTER PROCEDURE usp_UpdateUserAccount
(
@Username VARCHAR(64), @Username VARCHAR(64),
@FirstName NVARCHAR(128), @FirstName NVARCHAR(128),
@LastName NVARCHAR(128), @LastName NVARCHAR(128),
@@ -10,26 +8,20 @@ CREATE OR ALTER PROCEDURE usp_UpdateUserAccount
) )
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET
SET XACT_ABORT ON NOCOUNT ON;
BEGIN TRANSACTION
IF NOT EXISTS (SELECT 1 FROM UserAccount WHERE UserAccountId = @UserAccountId)
BEGIN
RAISERROR('UserAccount with the specified ID does not exist.', 16,
1);
ROLLBACK TRANSACTION
RETURN
END
UPDATE UserAccount UPDATE UserAccount
SET SET Username = @Username,
Username = @Username,
FirstName = @FirstName, FirstName = @FirstName,
LastName = @LastName, LastName = @LastName,
DateOfBirth = @DateOfBirth, DateOfBirth = @DateOfBirth,
Email = @Email Email = @Email
WHERE UserAccountId = @UserAccountId; WHERE UserAccountId = @UserAccountId;
COMMIT TRANSACTION IF @@ROWCOUNT = 0
BEGIN
THROW
50001, 'UserAccount with the specified ID does not exist.', 1;
END
END; END;

View File

@@ -4,7 +4,6 @@ CREATE OR ALTER PROCEDURE dbo.USP_GetActiveUserCredentialByUserAccountId(
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
SELECT SELECT
UserCredentialId, UserCredentialId,

View File

@@ -1,5 +1,5 @@
CREATE OR ALTER PROCEDURE dbo.USP_InvalidateUserCredential( CREATE OR ALTER PROCEDURE dbo.USP_InvalidateUserCredential(
@UserAccountId UNIQUEIDENTIFIER @UserAccountId_ UNIQUEIDENTIFIER
) )
AS AS
BEGIN BEGIN
@@ -8,17 +8,16 @@ BEGIN
BEGIN TRANSACTION; BEGIN TRANSACTION;
IF NOT EXISTS (SELECT 1 EXEC dbo.USP_GetUserAccountByID @UserAccountId = @UserAccountId_;
FROM dbo.UserAccount IF @@ROWCOUNT = 0
WHERE UserAccountID = @UserAccountId) THROW 50001, 'User account not found', 1;
ROLLBACK TRANSACTION
-- invalidate all other credentials by setting them to revoked -- invalidate all other credentials by setting them to revoked
UPDATE dbo.UserCredential UPDATE dbo.UserCredential
SET IsRevoked = 1, SET IsRevoked = 1,
RevokedAt = GETDATE() RevokedAt = GETDATE()
WHERE UserAccountId = @UserAccountId AND IsRevoked != 1; WHERE UserAccountId = @UserAccountId_
AND IsRevoked != 1;
COMMIT TRANSACTION; COMMIT TRANSACTION;

View File

@@ -0,0 +1,42 @@
CREATE OR ALTER PROCEDURE dbo.USP_RegisterUser(
@UserAccountId_ UNIQUEIDENTIFIER OUTPUT,
@Username VARCHAR(64),
@FirstName NVARCHAR(128),
@LastName NVARCHAR(128),
@DateOfBirth DATETIME,
@Email VARCHAR(128),
@Hash NVARCHAR(MAX)
)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRANSACTION;
EXEC usp_CreateUserAccount
@UserAccountId = @UserAccountId_ OUTPUT,
@Username = @Username,
@FirstName = @FirstName,
@LastName = @LastName,
@DateOfBirth = @DateOfBirth,
@Email = @Email;
IF @UserAccountId_ IS NULL
BEGIN
THROW 50000, 'Failed to create user account.', 1;
END
EXEC dbo.usp_RotateUserCredential
@UserAccountId = @UserAccountId_,
@Hash = @Hash;
IF @@ROWCOUNT = 0
BEGIN
THROW 50002, 'Failed to create user credential.', 1;
END
COMMIT TRANSACTION;
END

View File

@@ -1,30 +1,28 @@
CREATE OR ALTER PROCEDURE dbo.USP_RotateUserCredential( CREATE OR ALTER PROCEDURE dbo.USP_RotateUserCredential(
@UserAccountId UNIQUEIDENTIFIER, @UserAccountId_ UNIQUEIDENTIFIER,
@Hash NVARCHAR(MAX) @Hash NVARCHAR(MAX)
) )
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON; SET XACT_ABORT ON;
BEGIN TRANSACTION; BEGIN TRANSACTION;
IF NOT EXISTS (SELECT 1 EXEC USP_GetUserAccountByID @UserAccountId = @UserAccountId_
FROM dbo.UserAccount
WHERE UserAccountID = @UserAccountId) IF @@ROWCOUNT = 0
BEGIN THROW 50001, 'User account not found', 1;
ROLLBACK TRANSACTION;
END
-- invalidate all other credentials -- set them to revoked -- invalidate all other credentials -- set them to revoked
UPDATE dbo.UserCredential UPDATE dbo.UserCredential
SET IsRevoked = 1, SET IsRevoked = 1,
RevokedAt = GETDATE() RevokedAt = GETDATE()
WHERE UserAccountId = @UserAccountId; WHERE UserAccountId = @UserAccountId_;
INSERT INTO dbo.UserCredential INSERT INTO dbo.UserCredential
(UserAccountId, Hash) (UserAccountId, Hash)
VALUES (@UserAccountId, @Hash); VALUES (@UserAccountId_, @Hash);
COMMIT TRANSACTION;
END; END;

View File

@@ -1,6 +1,5 @@
CREATE OR ALTER PROCEDURE dbo.USP_CreateUserVerification CREATE OR ALTER PROCEDURE dbo.USP_CreateUserVerification @UserAccountID_ UNIQUEIDENTIFIER,
@UserAccountID uniqueidentifier, @VerificationDateTime DATETIME = NULL
@VerificationDateTime datetime = NULL
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
@@ -11,10 +10,13 @@ BEGIN
BEGIN TRANSACTION; BEGIN TRANSACTION;
EXEC USP_GetUserAccountByID @UserAccountId = @UserAccountID_;
IF @@ROWCOUNT = 0
THROW 50001, 'Could not find a user with that id', 1;
INSERT INTO dbo.UserVerification INSERT INTO dbo.UserVerification
(UserAccountId, VerificationDateTime) (UserAccountId, VerificationDateTime)
VALUES VALUES (@UserAccountID_, @VerificationDateTime);
(@UserAccountID, @VerificationDateTime);
COMMIT TRANSACTION; COMMIT TRANSACTION;
END END

View File

@@ -1,5 +1,4 @@
CREATE OR ALTER PROCEDURE dbo.USP_CreateCity CREATE OR ALTER PROCEDURE dbo.USP_CreateCity(
(
@CityName NVARCHAR(100), @CityName NVARCHAR(100),
@StateProvinceCode NVARCHAR(6) @StateProvinceCode NVARCHAR(6)
) )
@@ -8,23 +7,24 @@ BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON; SET XACT_ABORT ON;
BEGIN TRANSACTION
DECLARE @StateProvinceId UNIQUEIDENTIFIER = dbo.UDF_GetStateProvinceIdByCode(@StateProvinceCode); DECLARE @StateProvinceId UNIQUEIDENTIFIER = dbo.UDF_GetStateProvinceIdByCode(@StateProvinceCode);
IF @StateProvinceId IS NULL IF @StateProvinceId IS NULL
BEGIN BEGIN
RAISERROR('State/province not found for code.', 16, 1); THROW 50001, 'State/province does not exist', 1;
RETURN;
END END
IF EXISTS ( IF EXISTS (SELECT 1
SELECT 1
FROM dbo.City FROM dbo.City
WHERE CityName = @CityName WHERE CityName = @CityName
AND StateProvinceID = @StateProvinceId AND StateProvinceID = @StateProvinceId)
) BEGIN
RETURN;
THROW 50002, 'City already exists.', 1;
END
INSERT INTO dbo.City INSERT INTO dbo.City
(StateProvinceID, CityName) (StateProvinceID, CityName)
VALUES VALUES (@StateProvinceId, @CityName);
(@StateProvinceId, @CityName); COMMIT TRANSACTION
END; END;

View File

@@ -1,5 +1,4 @@
CREATE OR ALTER PROCEDURE dbo.USP_CreateCountry CREATE OR ALTER PROCEDURE dbo.USP_CreateCountry(
(
@CountryName NVARCHAR(100), @CountryName NVARCHAR(100),
@ISO3616_1 NVARCHAR(2) @ISO3616_1 NVARCHAR(2)
) )
@@ -7,16 +6,15 @@ AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON; SET XACT_ABORT ON;
BEGIN TRANSACTION;
IF EXISTS ( IF EXISTS (SELECT 1
SELECT 1
FROM dbo.Country FROM dbo.Country
WHERE ISO3616_1 = @ISO3616_1 WHERE ISO3616_1 = @ISO3616_1)
) THROW 50001, 'Country already exists', 1;
RETURN;
INSERT INTO dbo.Country INSERT INTO dbo.Country
(CountryName, ISO3616_1) (CountryName, ISO3616_1)
VALUES VALUES (@CountryName, @ISO3616_1);
(@CountryName, @ISO3616_1); COMMIT TRANSACTION;
END; END;

View File

@@ -1,5 +1,4 @@
CREATE OR ALTER PROCEDURE dbo.USP_CreateStateProvince CREATE OR ALTER PROCEDURE dbo.USP_CreateStateProvince(
(
@StateProvinceName NVARCHAR(100), @StateProvinceName NVARCHAR(100),
@ISO3616_2 NVARCHAR(6), @ISO3616_2 NVARCHAR(6),
@CountryCode NVARCHAR(2) @CountryCode NVARCHAR(2)
@@ -9,22 +8,19 @@ BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON; SET XACT_ABORT ON;
IF EXISTS ( IF EXISTS (SELECT 1
SELECT 1
FROM dbo.StateProvince FROM dbo.StateProvince
WHERE ISO3616_2 = @ISO3616_2 WHERE ISO3616_2 = @ISO3616_2)
)
RETURN; RETURN;
DECLARE @CountryId UNIQUEIDENTIFIER = dbo.UDF_GetCountryIdByCode(@CountryCode); DECLARE @CountryId UNIQUEIDENTIFIER = dbo.UDF_GetCountryIdByCode(@CountryCode);
IF @CountryId IS NULL IF @CountryId IS NULL
BEGIN BEGIN
RAISERROR('Country not found for code.', 16, 1); THROW 50001, 'Country does not exist', 1;
RETURN;
END END
INSERT INTO dbo.StateProvince INSERT INTO dbo.StateProvince
(StateProvinceName, ISO3616_2, CountryID) (StateProvinceName, ISO3616_2, CountryID)
VALUES VALUES (@StateProvinceName, @ISO3616_2, @CountryId);
(@StateProvinceName, @ISO3616_2, @CountryId);
END; END;

View File

@@ -1,12 +1,12 @@
using System.Data.Common;
using DataAccessLayer.Sql; using DataAccessLayer.Sql;
using Microsoft.Data.SqlClient;
namespace DataAccessLayer.Repositories namespace DataAccessLayer.Repositories
{ {
public abstract class Repository<T>(ISqlConnectionFactory connectionFactory) public abstract class Repository<T>(ISqlConnectionFactory connectionFactory)
where T : class where T : class
{ {
protected async Task<SqlConnection> CreateConnection() protected async Task<DbConnection> CreateConnection()
{ {
var connection = connectionFactory.CreateConnection(); var connection = connectionFactory.CreateConnection();
await connection.OpenAsync(); await connection.OpenAsync();
@@ -19,6 +19,6 @@ namespace DataAccessLayer.Repositories
public abstract Task UpdateAsync(T entity); public abstract Task UpdateAsync(T entity);
public abstract Task DeleteAsync(Guid id); public abstract Task DeleteAsync(Guid id);
protected abstract T MapToEntity(SqlDataReader reader); protected abstract T MapToEntity(DbDataReader reader);
} }
} }

View File

@@ -1,24 +1,28 @@
using System.Data; using System.Data;
using System.Data.Common;
using DataAccessLayer.Sql; using DataAccessLayer.Sql;
using Microsoft.Data.SqlClient;
namespace DataAccessLayer.Repositories.UserAccount namespace DataAccessLayer.Repositories.UserAccount
{ {
public class UserAccountRepository(ISqlConnectionFactory connectionFactory) public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
: Repository<Entities.UserAccount>(connectionFactory), IUserAccountRepository : Repository<Entities.UserAccount>(connectionFactory), IUserAccountRepository
{ {
/**
* @todo update the create user account stored proc to add user credential creation in
* a single transaction, use that transaction instead.
*/
public override async Task AddAsync(Entities.UserAccount userAccount) public override async Task AddAsync(Entities.UserAccount userAccount)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = new SqlCommand("usp_CreateUserAccount", connection); await using var command = connection.CreateCommand();
command.CommandText = "usp_CreateUserAccount";
command.CommandType = CommandType.StoredProcedure; command.CommandType = CommandType.StoredProcedure;
AddParameter(command, "@UserAccountId", userAccount.UserAccountId);
command.Parameters.Add("@UserAccountId", SqlDbType.UniqueIdentifier).Value = userAccount.UserAccountId; AddParameter(command, "@Username", userAccount.Username);
command.Parameters.Add("@Username", SqlDbType.NVarChar, 100).Value = userAccount.Username; AddParameter(command, "@FirstName", userAccount.FirstName);
command.Parameters.Add("@FirstName", SqlDbType.NVarChar, 100).Value = userAccount.FirstName; AddParameter(command, "@LastName", userAccount.LastName);
command.Parameters.Add("@LastName", SqlDbType.NVarChar, 100).Value = userAccount.LastName; AddParameter(command, "@Email", userAccount.Email);
command.Parameters.Add("@Email", SqlDbType.NVarChar, 256).Value = userAccount.Email; AddParameter(command, "@DateOfBirth", userAccount.DateOfBirth);
command.Parameters.Add("@DateOfBirth", SqlDbType.Date).Value = userAccount.DateOfBirth;
await command.ExecuteNonQueryAsync(); await command.ExecuteNonQueryAsync();
} }
@@ -26,12 +30,11 @@ namespace DataAccessLayer.Repositories.UserAccount
public override async Task<Entities.UserAccount?> GetByIdAsync(Guid id) public override async Task<Entities.UserAccount?> GetByIdAsync(Guid id)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = new SqlCommand("usp_GetUserAccountById", connection) await using var command = connection.CreateCommand();
{ command.CommandText = "usp_GetUserAccountById";
CommandType = CommandType.StoredProcedure command.CommandType = CommandType.StoredProcedure;
};
command.Parameters.Add("@UserAccountId", SqlDbType.UniqueIdentifier).Value = id; AddParameter(command, "@UserAccountId", id);
await using var reader = await command.ExecuteReaderAsync(); await using var reader = await command.ExecuteReaderAsync();
return await reader.ReadAsync() ? MapToEntity(reader) : null; return await reader.ReadAsync() ? MapToEntity(reader) : null;
@@ -40,14 +43,15 @@ namespace DataAccessLayer.Repositories.UserAccount
public override async Task<IEnumerable<Entities.UserAccount>> GetAllAsync(int? limit, int? offset) public override async Task<IEnumerable<Entities.UserAccount>> GetAllAsync(int? limit, int? offset)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = new SqlCommand("usp_GetAllUserAccounts", connection); await using var command = connection.CreateCommand();
command.CommandText = "usp_GetAllUserAccounts";
command.CommandType = CommandType.StoredProcedure; command.CommandType = CommandType.StoredProcedure;
if (limit.HasValue) if (limit.HasValue)
command.Parameters.Add("@Limit", SqlDbType.Int).Value = limit.Value; AddParameter(command, "@Limit", limit.Value);
if (offset.HasValue) if (offset.HasValue)
command.Parameters.Add("@Offset", SqlDbType.Int).Value = offset.Value; AddParameter(command, "@Offset", offset.Value);
await using var reader = await command.ExecuteReaderAsync(); await using var reader = await command.ExecuteReaderAsync();
var users = new List<Entities.UserAccount>(); var users = new List<Entities.UserAccount>();
@@ -63,15 +67,16 @@ namespace DataAccessLayer.Repositories.UserAccount
public override async Task UpdateAsync(Entities.UserAccount userAccount) public override async Task UpdateAsync(Entities.UserAccount userAccount)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = new SqlCommand("usp_UpdateUserAccount", connection); await using var command = connection.CreateCommand();
command.CommandText = "usp_UpdateUserAccount";
command.CommandType = CommandType.StoredProcedure; command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add("@UserAccountId", SqlDbType.UniqueIdentifier).Value = userAccount.UserAccountId; AddParameter(command, "@UserAccountId", userAccount.UserAccountId);
command.Parameters.Add("@Username", SqlDbType.NVarChar, 100).Value = userAccount.Username; AddParameter(command, "@Username", userAccount.Username);
command.Parameters.Add("@FirstName", SqlDbType.NVarChar, 100).Value = userAccount.FirstName; AddParameter(command, "@FirstName", userAccount.FirstName);
command.Parameters.Add("@LastName", SqlDbType.NVarChar, 100).Value = userAccount.LastName; AddParameter(command, "@LastName", userAccount.LastName);
command.Parameters.Add("@Email", SqlDbType.NVarChar, 256).Value = userAccount.Email; AddParameter(command, "@Email", userAccount.Email);
command.Parameters.Add("@DateOfBirth", SqlDbType.Date).Value = userAccount.DateOfBirth; AddParameter(command, "@DateOfBirth", userAccount.DateOfBirth);
await command.ExecuteNonQueryAsync(); await command.ExecuteNonQueryAsync();
} }
@@ -79,20 +84,22 @@ namespace DataAccessLayer.Repositories.UserAccount
public override async Task DeleteAsync(Guid id) public override async Task DeleteAsync(Guid id)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = new SqlCommand("usp_DeleteUserAccount", connection); await using var command = connection.CreateCommand();
command.CommandText = "usp_DeleteUserAccount";
command.CommandType = CommandType.StoredProcedure; command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add("@UserAccountId", SqlDbType.UniqueIdentifier).Value = id; AddParameter(command, "@UserAccountId", id);
await command.ExecuteNonQueryAsync(); await command.ExecuteNonQueryAsync();
} }
public async Task<Entities.UserAccount?> GetByUsernameAsync(string username) public async Task<Entities.UserAccount?> GetByUsernameAsync(string username)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = new SqlCommand("usp_GetUserAccountByUsername", connection); await using var command = connection.CreateCommand();
command.CommandText = "usp_GetUserAccountByUsername";
command.CommandType = CommandType.StoredProcedure; command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add("@Username", SqlDbType.NVarChar, 100).Value = username; AddParameter(command, "@Username", username);
await using var reader = await command.ExecuteReaderAsync(); await using var reader = await command.ExecuteReaderAsync();
return await reader.ReadAsync() ? MapToEntity(reader) : null; return await reader.ReadAsync() ? MapToEntity(reader) : null;
@@ -101,16 +108,17 @@ namespace DataAccessLayer.Repositories.UserAccount
public async Task<Entities.UserAccount?> GetByEmailAsync(string email) public async Task<Entities.UserAccount?> GetByEmailAsync(string email)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = new SqlCommand("usp_GetUserAccountByEmail", connection); await using var command = connection.CreateCommand();
command.CommandText = "usp_GetUserAccountByEmail";
command.CommandType = CommandType.StoredProcedure; command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add("@Email", SqlDbType.NVarChar, 256).Value = email; AddParameter(command, "@Email", email);
await using var reader = await command.ExecuteReaderAsync(); await using var reader = await command.ExecuteReaderAsync();
return await reader.ReadAsync() ? MapToEntity(reader) : null; return await reader.ReadAsync() ? MapToEntity(reader) : null;
} }
protected override Entities.UserAccount MapToEntity(SqlDataReader reader) protected override Entities.UserAccount MapToEntity(DbDataReader reader)
{ {
return new Entities.UserAccount return new Entities.UserAccount
{ {
@@ -129,5 +137,13 @@ namespace DataAccessLayer.Repositories.UserAccount
: (byte[])reader["Timer"] : (byte[])reader["Timer"]
}; };
} }
private static void AddParameter(DbCommand command, string name, object? value)
{
var p = command.CreateParameter();
p.ParameterName = name;
p.Value = value ?? DBNull.Value;
command.Parameters.Add(p);
}
} }
} }

View File

@@ -4,4 +4,5 @@ public interface IUserCredentialRepository
{ {
Task RotateCredentialAsync(Guid userAccountId, UserCredential credential); Task RotateCredentialAsync(Guid userAccountId, UserCredential credential);
Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(Guid userAccountId); Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(Guid userAccountId);
Task InvalidateCredentialsByUserAccountIdAsync(Guid userAccountId);
} }

View File

@@ -0,0 +1,93 @@
using System.Data;
using System.Data.Common;
using DataAccessLayer.Sql;
namespace DataAccessLayer.Repositories.UserCredential
{
public class UserCredentialRepository(ISqlConnectionFactory connectionFactory)
: DataAccessLayer.Repositories.Repository<Entities.UserCredential>(connectionFactory), IUserCredentialRepository
{
public async Task RotateCredentialAsync(Guid userAccountId, Entities.UserCredential credential)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
command.CommandText = "USP_RotateUserCredential";
command.CommandType = CommandType.StoredProcedure;
AddParameter(command, "@UserAccountId", userAccountId);
AddParameter(command, "@Hash", credential.Hash);
await command.ExecuteNonQueryAsync();
}
public async Task<Entities.UserCredential?> GetActiveCredentialByUserAccountIdAsync(Guid userAccountId)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
command.CommandText = "USP_GetActiveUserCredentialByUserAccountId";
command.CommandType = CommandType.StoredProcedure;
AddParameter(command, "@UserAccountId", userAccountId);
await using var reader = await command.ExecuteReaderAsync();
return await reader.ReadAsync() ? MapToEntity(reader) : null;
}
public async Task InvalidateCredentialsByUserAccountIdAsync(Guid userAccountId)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
command.CommandText = "USP_InvalidateUserCredential";
command.CommandType = CommandType.StoredProcedure;
AddParameter(command, "@UserAccountId", userAccountId);
await command.ExecuteNonQueryAsync();
}
public override Task AddAsync(Entities.UserCredential entity)
=> throw new NotSupportedException("Use RotateCredentialAsync for adding/rotating credentials.");
public override Task<IEnumerable<Entities.UserCredential>> GetAllAsync(int? limit, int? offset)
=> throw new NotSupportedException("Listing credentials is not supported.");
public override Task<Entities.UserCredential?> GetByIdAsync(Guid id)
=> throw new NotSupportedException("Fetching credential by ID is not supported.");
public override Task UpdateAsync(Entities.UserCredential entity)
=> throw new NotSupportedException("Use RotateCredentialAsync to update credentials.");
public override Task DeleteAsync(Guid id)
=> throw new NotSupportedException("Deleting a credential by ID is not supported.");
protected override Entities.UserCredential MapToEntity(DbDataReader reader)
{
var entity = new Entities.UserCredential
{
UserCredentialId = reader.GetGuid(reader.GetOrdinal("UserCredentialId")),
UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")),
Hash = reader.GetString(reader.GetOrdinal("Hash")),
CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt"))
};
// Optional columns
var hasTimer = reader.GetSchemaTable()?.Rows
.Cast<System.Data.DataRow>()
.Any(r => string.Equals(r["ColumnName"]?.ToString(), "Timer", StringComparison.OrdinalIgnoreCase)) ?? false;
if (hasTimer)
{
entity.Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) ? null : (byte[])reader["Timer"];
}
return entity;
}
private static void AddParameter(DbCommand command, string name, object? value)
{
var p = command.CreateParameter();
p.ParameterName = name;
p.Value = value ?? DBNull.Value;
command.Parameters.Add(p);
}
}
}

View File

@@ -1,3 +1,4 @@
using System.Data.Common;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@@ -12,7 +13,7 @@ namespace DataAccessLayer.Sql
"Database connection string not configured. Set DB_CONNECTION_STRING env var or ConnectionStrings:Default." "Database connection string not configured. Set DB_CONNECTION_STRING env var or ConnectionStrings:Default."
); );
public SqlConnection CreateConnection() public DbConnection CreateConnection()
{ {
return new SqlConnection(_connectionString); return new SqlConnection(_connectionString);
} }

View File

@@ -1,9 +1,9 @@
using Microsoft.Data.SqlClient; using System.Data.Common;
namespace DataAccessLayer.Sql namespace DataAccessLayer.Sql
{ {
public interface ISqlConnectionFactory public interface ISqlConnectionFactory
{ {
SqlConnection CreateConnection(); DbConnection CreateConnection();
} }
} }

View File

@@ -0,0 +1,73 @@
using DataAccessLayer.Sql;
using FluentAssertions;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
namespace Repository.Tests.Database;
public class DefaultSqlConnectionFactoryTest
{
private static IConfiguration EmptyConfig()
=> new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>()).Build();
[Fact]
public void CreateConnection_Uses_EnvVar_WhenAvailable()
{
var previous = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING");
try
{
Environment.SetEnvironmentVariable("DB_CONNECTION_STRING", "Server=localhost;Database=TestDb;Trusted_Connection=True;Encrypt=False");
var factory = new DefaultSqlConnectionFactory(EmptyConfig());
var conn = factory.CreateConnection();
conn.Should().BeOfType<SqlConnection>();
conn.ConnectionString.Should().Contain("Database=TestDb");
}
finally
{
Environment.SetEnvironmentVariable("DB_CONNECTION_STRING", previous);
}
}
[Fact]
public void CreateConnection_Uses_Config_WhenEnvMissing()
{
var previous = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING");
try
{
Environment.SetEnvironmentVariable("DB_CONNECTION_STRING", null);
var cfg = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "ConnectionStrings:Default", "Server=localhost;Database=CfgDb;Trusted_Connection=True;Encrypt=False" }
})
.Build();
var factory = new DefaultSqlConnectionFactory(cfg);
var conn = factory.CreateConnection();
conn.ConnectionString.Should().Contain("Database=CfgDb");
}
finally
{
Environment.SetEnvironmentVariable("DB_CONNECTION_STRING", previous);
}
}
[Fact]
public void Constructor_Throws_When_NoEnv_NoConfig()
{
var previous = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING");
try
{
Environment.SetEnvironmentVariable("DB_CONNECTION_STRING", null);
var cfg = EmptyConfig();
Action act = () => _ = new DefaultSqlConnectionFactory(cfg);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Database connection string not configured*");
}
finally
{
Environment.SetEnvironmentVariable("DB_CONNECTION_STRING", previous);
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Data.Common;
using DataAccessLayer.Sql;
namespace Repository.Tests.Database;
internal class TestConnectionFactory(DbConnection conn) : ISqlConnectionFactory
{
private readonly DbConnection _conn = conn;
public DbConnection CreateConnection() => _conn;
}

View File

@@ -4,14 +4,21 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<RootNamespace>DALTests</RootNamespace> <RootNamespace>Repository.Tests</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" /> <PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="FluentAssertions" Version="6.9.0" />
<PackageReference Include="DbMocker" Version="1.26.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,136 @@
using Apps72.Dev.Data.DbMocker;
using DataAccessLayer.Repositories.UserAccount;
using FluentAssertions;
using Repository.Tests.Database;
namespace Repository.Tests.UserAccount;
public class UserAccountRepositoryTest
{
private static UserAccountRepository CreateRepo(MockDbConnection conn)
=> new(new TestConnectionFactory(conn));
[Fact]
public async Task GetByIdAsync_ReturnsRow_Mapped()
{
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountById")
.ReturnsTable(MockTable.WithColumns(
("UserAccountId", typeof(Guid)),
("Username", typeof(string)),
("FirstName", typeof(string)),
("LastName", typeof(string)),
("Email", typeof(string)),
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[]))
).AddRow(Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
"yerb","Aaron","Po","aaronpo@example.com",
new DateTime(2020,1,1), null,
new DateTime(1990,1,1), null));
var repo = CreateRepo(conn);
var result = await repo.GetByIdAsync(Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"));
result.Should().NotBeNull();
result!.Username.Should().Be("yerb");
result.Email.Should().Be("aaronpo@example.com");
}
[Fact]
public async Task GetAllAsync_ReturnsMultipleRows()
{
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetAllUserAccounts")
.ReturnsTable(MockTable.WithColumns(
("UserAccountId", typeof(Guid)),
("Username", typeof(string)),
("FirstName", typeof(string)),
("LastName", typeof(string)),
("Email", typeof(string)),
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[]))
).AddRow(Guid.NewGuid(), "a","A","A","a@example.com", DateTime.UtcNow, null, DateTime.UtcNow.Date, null)
.AddRow(Guid.NewGuid(), "b","B","B","b@example.com", DateTime.UtcNow, null, DateTime.UtcNow.Date, null));
var repo = CreateRepo(conn);
var results = (await repo.GetAllAsync(null, null)).ToList();
results.Should().HaveCount(2);
results.Select(r => r.Username).Should().BeEquivalentTo(new[] { "a", "b" });
}
[Fact]
public async Task AddAsync_ExecutesStoredProcedure()
{
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_CreateUserAccount")
.ReturnsScalar(1);
var repo = CreateRepo(conn);
var user = new DataAccessLayer.Entities.UserAccount
{
UserAccountId = Guid.NewGuid(),
Username = "newuser",
FirstName = "New",
LastName = "User",
Email = "newuser@example.com",
DateOfBirth = new DateTime(1991,1,1)
};
await repo.AddAsync(user);
}
[Fact]
public async Task GetByUsername_ReturnsRow()
{
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername")
.ReturnsTable(MockTable.WithColumns(
("UserAccountId", typeof(Guid)),
("Username", typeof(string)),
("FirstName", typeof(string)),
("LastName", typeof(string)),
("Email", typeof(string)),
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[]))
).AddRow(Guid.NewGuid(), "lookupuser","L","U","lookup@example.com", DateTime.UtcNow, null, DateTime.UtcNow.Date, null));
var repo = CreateRepo(conn);
var result = await repo.GetByUsernameAsync("lookupuser");
result.Should().NotBeNull();
result!.Email.Should().Be("lookup@example.com");
}
[Fact]
public async Task GetByEmail_ReturnsRow()
{
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.ReturnsTable(MockTable.WithColumns(
("UserAccountId", typeof(Guid)),
("Username", typeof(string)),
("FirstName", typeof(string)),
("LastName", typeof(string)),
("Email", typeof(string)),
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[]))
).AddRow(Guid.NewGuid(), "byemail","B","E","byemail@example.com", DateTime.UtcNow, null, DateTime.UtcNow.Date, null));
var repo = CreateRepo(conn);
var result = await repo.GetByEmailAsync("byemail@example.com");
result.Should().NotBeNull();
result!.Username.Should().Be("byemail");
}
}

View File

@@ -1,281 +0,0 @@
using DataAccessLayer;
using DataAccessLayer.Entities;
using DataAccessLayer.Repositories;
using DataAccessLayer.Repositories.UserAccount;
namespace DALTests
{
public class UserAccountRepositoryTests
{
private readonly IUserAccountRepository _repository = new InMemoryUserAccountRepository();
[Fact]
public async Task Add_ShouldInsertUserAccount()
{
// Arrange
var userAccount = new UserAccount
{
UserAccountId = Guid.NewGuid(),
Username = "testuser",
FirstName = "Test",
LastName = "User",
Email = "testuser@example.com",
CreatedAt = DateTime.UtcNow,
DateOfBirth = new DateTime(1990, 1, 1),
};
// Act
await _repository.AddAsync(userAccount);
var retrievedUser = await _repository.GetByIdAsync(userAccount.UserAccountId);
// Assert
Assert.NotNull(retrievedUser);
Assert.Equal(userAccount.Username, retrievedUser.Username);
}
[Fact]
public async Task GetById_ShouldReturnUserAccount()
{
// Arrange
var userId = Guid.NewGuid();
var userAccount = new UserAccount
{
UserAccountId = userId,
Username = "existinguser",
FirstName = "Existing",
LastName = "User",
Email = "existinguser@example.com",
CreatedAt = DateTime.UtcNow,
DateOfBirth = new DateTime(1985, 5, 15),
};
await _repository.AddAsync(userAccount);
// Act
var retrievedUser = await _repository.GetByIdAsync(userId);
// Assert
Assert.NotNull(retrievedUser);
Assert.Equal(userId, retrievedUser.UserAccountId);
}
[Fact]
public async Task Update_ShouldModifyUserAccount()
{
// Arrange
var userAccount = new UserAccount
{
UserAccountId = Guid.NewGuid(),
Username = "updatableuser",
FirstName = "Updatable",
LastName = "User",
Email = "updatableuser@example.com",
CreatedAt = DateTime.UtcNow,
DateOfBirth = new DateTime(1992, 3, 10),
};
await _repository.AddAsync(userAccount);
// Act
userAccount.FirstName = "Updated";
await _repository.UpdateAsync(userAccount);
var updatedUser = await _repository.GetByIdAsync(userAccount.UserAccountId);
// Assert
Assert.NotNull(updatedUser);
Assert.Equal("Updated", updatedUser.FirstName);
}
[Fact]
public async Task Delete_ShouldRemoveUserAccount()
{
// Arrange
var userAccount = new UserAccount
{
UserAccountId = Guid.NewGuid(),
Username = "deletableuser",
FirstName = "Deletable",
LastName = "User",
Email = "deletableuser@example.com",
CreatedAt = DateTime.UtcNow,
DateOfBirth = new DateTime(1995, 7, 20),
};
await _repository.AddAsync(userAccount);
// Act
await _repository.DeleteAsync(userAccount.UserAccountId);
var deletedUser = await _repository.GetByIdAsync(userAccount.UserAccountId);
// Assert
Assert.Null(deletedUser);
}
[Fact]
public async Task GetAll_ShouldReturnAllUserAccounts()
{
// Arrange
var user1 = new UserAccount
{
UserAccountId = Guid.NewGuid(),
Username = "user1",
FirstName = "User",
LastName = "One",
Email = "user1@example.com",
CreatedAt = DateTime.UtcNow,
DateOfBirth = new DateTime(1990, 1, 1),
};
var user2 = new UserAccount
{
UserAccountId = Guid.NewGuid(),
Username = "user2",
FirstName = "User",
LastName = "Two",
Email = "user2@example.com",
CreatedAt = DateTime.UtcNow,
DateOfBirth = new DateTime(1992, 2, 2),
};
await _repository.AddAsync(user1);
await _repository.AddAsync(user2);
// Act
var allUsers = await _repository.GetAllAsync(null, null);
// Assert
Assert.NotNull(allUsers);
Assert.True(allUsers.Count() >= 2);
}
[Fact]
public async Task GetAll_WithPagination_ShouldRespectLimit()
{
// Arrange
var users = new List<UserAccount>
{
new UserAccount
{
UserAccountId = Guid.NewGuid(),
Username = $"pageuser_{Guid.NewGuid():N}",
FirstName = "Page",
LastName = "User",
Email = $"pageuser_{Guid.NewGuid():N}@example.com",
CreatedAt = DateTime.UtcNow,
DateOfBirth = new DateTime(1991, 4, 4),
},
new UserAccount
{
UserAccountId = Guid.NewGuid(),
Username = $"pageuser_{Guid.NewGuid():N}",
FirstName = "Page",
LastName = "User",
Email = $"pageuser_{Guid.NewGuid():N}@example.com",
CreatedAt = DateTime.UtcNow,
DateOfBirth = new DateTime(1992, 5, 5),
},
new UserAccount
{
UserAccountId = Guid.NewGuid(),
Username = $"pageuser_{Guid.NewGuid():N}",
FirstName = "Page",
LastName = "User",
Email = $"pageuser_{Guid.NewGuid():N}@example.com",
CreatedAt = DateTime.UtcNow,
DateOfBirth = new DateTime(1993, 6, 6),
},
};
foreach (var user in users)
{
await _repository.AddAsync(user);
}
// Act
var page = (await _repository.GetAllAsync(2, 0)).ToList();
// Assert
Assert.Equal(2, page.Count);
}
[Fact]
public async Task GetAll_WithPagination_ShouldValidateArguments()
{
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
(await _repository.GetAllAsync(0, 0)).ToList()
);
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
(await _repository.GetAllAsync(1, -1)).ToList()
);
}
}
internal class InMemoryUserAccountRepository : IUserAccountRepository
{
private readonly Dictionary<Guid, UserAccount> _store = new();
public Task AddAsync(UserAccount userAccount)
{
if (userAccount.UserAccountId == Guid.Empty)
{
userAccount.UserAccountId = Guid.NewGuid();
}
_store[userAccount.UserAccountId] = Clone(userAccount);
return Task.CompletedTask;
}
public Task<UserAccount?> GetByIdAsync(Guid id)
{
_store.TryGetValue(id, out var user);
return Task.FromResult(user is null ? null : Clone(user));
}
public Task<IEnumerable<UserAccount>> GetAllAsync(int? limit, int? offset)
{
if (limit.HasValue && limit.Value <= 0) throw new ArgumentOutOfRangeException(nameof(limit));
if (offset.HasValue && offset.Value < 0) throw new ArgumentOutOfRangeException(nameof(offset));
var query = _store.Values
.OrderBy(u => u.Username)
.Select(Clone);
if (offset.HasValue) query = query.Skip(offset.Value);
if (limit.HasValue) query = query.Take(limit.Value);
return Task.FromResult<IEnumerable<UserAccount>>(query.ToList());
}
public Task UpdateAsync(UserAccount userAccount)
{
if (!_store.ContainsKey(userAccount.UserAccountId)) return Task.CompletedTask;
_store[userAccount.UserAccountId] = Clone(userAccount);
return Task.CompletedTask;
}
public Task DeleteAsync(Guid id)
{
_store.Remove(id);
return Task.CompletedTask;
}
public Task<UserAccount?> GetByUsernameAsync(string username)
{
var user = _store.Values.FirstOrDefault(u => u.Username == username);
return Task.FromResult(user is null ? null : Clone(user));
}
public Task<UserAccount?> GetByEmailAsync(string email)
{
var user = _store.Values.FirstOrDefault(u => u.Email == email);
return Task.FromResult(user is null ? null : Clone(user));
}
private static UserAccount Clone(UserAccount u) => new()
{
UserAccountId = u.UserAccountId,
Username = u.Username,
FirstName = u.FirstName,
LastName = u.LastName,
Email = u.Email,
CreatedAt = u.CreatedAt,
UpdatedAt = u.UpdatedAt,
DateOfBirth = u.DateOfBirth,
Timer = u.Timer is null ? null : (byte[])u.Timer.Clone(),
};
}
}

View File

@@ -0,0 +1,75 @@
using Apps72.Dev.Data.DbMocker;
using DataAccessLayer.Repositories.UserCredential;
using DataAccessLayer.Sql;
using FluentAssertions;
using Moq;
using Repository.Tests.Database;
namespace Repository.Tests.UserCredential;
public class UserCredentialRepositoryTests
{
private static UserCredentialRepository CreateRepo()
{
var factoryMock = new Mock<ISqlConnectionFactory>(MockBehavior.Strict);
// NotSupported methods do not use the factory; keep strict to ensure no unexpected calls.
return new UserCredentialRepository(factoryMock.Object);
}
[Fact]
public async Task AddAsync_ShouldThrow_NotSupported()
{
var repo = CreateRepo();
var act = async () => await repo.AddAsync(new DataAccessLayer.Entities.UserCredential());
await act.Should().ThrowAsync<NotSupportedException>();
}
[Fact]
public async Task GetAllAsync_ShouldThrow_NotSupported()
{
var repo = CreateRepo();
var act = async () => await repo.GetAllAsync(null, null);
await act.Should().ThrowAsync<NotSupportedException>();
}
[Fact]
public async Task GetByIdAsync_ShouldThrow_NotSupported()
{
var repo = CreateRepo();
var act = async () => await repo.GetByIdAsync(Guid.NewGuid());
await act.Should().ThrowAsync<NotSupportedException>();
}
[Fact]
public async Task UpdateAsync_ShouldThrow_NotSupported()
{
var repo = CreateRepo();
var act = async () => await repo.UpdateAsync(new DataAccessLayer.Entities.UserCredential());
await act.Should().ThrowAsync<NotSupportedException>();
}
[Fact]
public async Task DeleteAsync_ShouldThrow_NotSupported()
{
var repo = CreateRepo();
var act = async () => await repo.DeleteAsync(Guid.NewGuid());
await act.Should().ThrowAsync<NotSupportedException>();
}
[Fact]
public async Task RotateCredentialAsync_ExecutesWithoutError()
{
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "USP_RotateUserCredential")
.ReturnsRow(0);
var repo = new UserCredentialRepository(new TestConnectionFactory(conn));
var credential = new DataAccessLayer.Entities.UserCredential
{
Hash = "hashed_password"
};
await repo.RotateCredentialAsync(Guid.NewGuid(), credential);
}
}