mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
Refactor repository and SQL procedures; add repo tests
This commit is contained in:
@@ -171,7 +171,6 @@ CREATE TABLE UserCredential -- delete credentials when user account is deleted
|
||||
|
||||
Timer ROWVERSION,
|
||||
|
||||
|
||||
CONSTRAINT PK_UserCredential
|
||||
PRIMARY KEY (UserCredentialID),
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_CreateUserAccount
|
||||
(
|
||||
@UserAccountId UNIQUEIDENTIFIER = NULL,
|
||||
@UserAccountId UNIQUEIDENTIFIER OUTPUT,
|
||||
@Username VARCHAR(64),
|
||||
@FirstName NVARCHAR(128),
|
||||
@LastName NVARCHAR(128),
|
||||
@@ -10,13 +10,10 @@ CREATE OR ALTER PROCEDURE usp_CreateUserAccount
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
SET XACT_ABORT ON
|
||||
BEGIN TRANSACTION
|
||||
SET NOCOUNT ON;
|
||||
|
||||
INSERT INTO UserAccount
|
||||
(
|
||||
UserAccountID,
|
||||
Username,
|
||||
FirstName,
|
||||
LastName,
|
||||
@@ -25,12 +22,12 @@ BEGIN
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
COALESCE(@UserAccountId, NEWID()),
|
||||
@Username,
|
||||
@FirstName,
|
||||
@LastName,
|
||||
@DateOfBirth,
|
||||
@Email
|
||||
);
|
||||
COMMIT TRANSACTION
|
||||
|
||||
SELECT @UserAccountId AS UserAccountId;
|
||||
END;
|
||||
|
||||
@@ -6,8 +6,6 @@ CREATE OR ALTER PROCEDURE usp_DeleteUserAccount
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
SET XACT_ABORT ON
|
||||
BEGIN TRANSACTION
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM UserAccount WHERE UserAccountId = @UserAccountId)
|
||||
BEGIN
|
||||
@@ -19,5 +17,4 @@ BEGIN
|
||||
|
||||
DELETE FROM UserAccount
|
||||
WHERE UserAccountId = @UserAccountId;
|
||||
COMMIT TRANSACTION
|
||||
END;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_GetAllUserAccounts
|
||||
AS
|
||||
BEGIN
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_GetUserAccountByEmail
|
||||
(
|
||||
CREATE OR ALTER PROCEDURE usp_GetUserAccountByEmail(
|
||||
@Email VARCHAR(128)
|
||||
)
|
||||
AS
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_GetUserAccountById
|
||||
(
|
||||
CREATE OR ALTER PROCEDURE USP_GetUserAccountById(
|
||||
@UserAccountId UNIQUEIDENTIFIER
|
||||
)
|
||||
AS
|
||||
@@ -18,4 +16,4 @@ BEGIN
|
||||
Timer
|
||||
FROM dbo.UserAccount
|
||||
WHERE UserAccountID = @UserAccountId;
|
||||
END;
|
||||
END
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_GetUserAccountByUsername
|
||||
(
|
||||
CREATE OR ALTER PROCEDURE usp_GetUserAccountByUsername(
|
||||
@Username VARCHAR(64)
|
||||
)
|
||||
AS
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_UpdateUserAccount
|
||||
(
|
||||
CREATE OR ALTER PROCEDURE usp_UpdateUserAccount(
|
||||
@Username VARCHAR(64),
|
||||
@FirstName NVARCHAR(128),
|
||||
@LastName NVARCHAR(128),
|
||||
@@ -10,26 +8,20 @@ CREATE OR ALTER PROCEDURE usp_UpdateUserAccount
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
SET XACT_ABORT 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
|
||||
SET
|
||||
NOCOUNT ON;
|
||||
|
||||
UPDATE UserAccount
|
||||
SET
|
||||
Username = @Username,
|
||||
FirstName = @FirstName,
|
||||
LastName = @LastName,
|
||||
SET Username = @Username,
|
||||
FirstName = @FirstName,
|
||||
LastName = @LastName,
|
||||
DateOfBirth = @DateOfBirth,
|
||||
Email = @Email
|
||||
Email = @Email
|
||||
WHERE UserAccountId = @UserAccountId;
|
||||
|
||||
COMMIT TRANSACTION
|
||||
IF @@ROWCOUNT = 0
|
||||
BEGIN
|
||||
THROW
|
||||
50001, 'UserAccount with the specified ID does not exist.', 1;
|
||||
END
|
||||
END;
|
||||
|
||||
@@ -4,7 +4,6 @@ CREATE OR ALTER PROCEDURE dbo.USP_GetActiveUserCredentialByUserAccountId(
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
SELECT
|
||||
UserCredentialId,
|
||||
@@ -1,5 +1,5 @@
|
||||
CREATE OR ALTER PROCEDURE dbo.USP_InvalidateUserCredential(
|
||||
@UserAccountId UNIQUEIDENTIFIER
|
||||
@UserAccountId_ UNIQUEIDENTIFIER
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
@@ -8,17 +8,16 @@ BEGIN
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
IF NOT EXISTS (SELECT 1
|
||||
FROM dbo.UserAccount
|
||||
WHERE UserAccountID = @UserAccountId)
|
||||
ROLLBACK TRANSACTION
|
||||
|
||||
EXEC dbo.USP_GetUserAccountByID @UserAccountId = @UserAccountId_;
|
||||
IF @@ROWCOUNT = 0
|
||||
THROW 50001, 'User account not found', 1;
|
||||
|
||||
-- invalidate all other credentials by setting them to revoked
|
||||
UPDATE dbo.UserCredential
|
||||
SET IsRevoked = 1,
|
||||
RevokedAt = GETDATE()
|
||||
WHERE UserAccountId = @UserAccountId AND IsRevoked != 1;
|
||||
WHERE UserAccountId = @UserAccountId_
|
||||
AND IsRevoked != 1;
|
||||
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
@@ -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
|
||||
@@ -1,30 +1,28 @@
|
||||
CREATE OR ALTER PROCEDURE dbo.USP_RotateUserCredential(
|
||||
@UserAccountId UNIQUEIDENTIFIER,
|
||||
@UserAccountId_ UNIQUEIDENTIFIER,
|
||||
@Hash NVARCHAR(MAX)
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
IF NOT EXISTS (SELECT 1
|
||||
FROM dbo.UserAccount
|
||||
WHERE UserAccountID = @UserAccountId)
|
||||
BEGIN
|
||||
ROLLBACK TRANSACTION;
|
||||
END
|
||||
EXEC USP_GetUserAccountByID @UserAccountId = @UserAccountId_
|
||||
|
||||
IF @@ROWCOUNT = 0
|
||||
THROW 50001, 'User account not found', 1;
|
||||
|
||||
|
||||
-- invalidate all other credentials -- set them to revoked
|
||||
UPDATE dbo.UserCredential
|
||||
SET IsRevoked = 1,
|
||||
RevokedAt = GETDATE()
|
||||
WHERE UserAccountId = @UserAccountId;
|
||||
WHERE UserAccountId = @UserAccountId_;
|
||||
|
||||
INSERT INTO dbo.UserCredential
|
||||
(UserAccountId, Hash)
|
||||
VALUES (@UserAccountId, @Hash);
|
||||
VALUES (@UserAccountId_, @Hash);
|
||||
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
END;
|
||||
@@ -1,6 +1,5 @@
|
||||
CREATE OR ALTER PROCEDURE dbo.USP_CreateUserVerification
|
||||
@UserAccountID uniqueidentifier,
|
||||
@VerificationDateTime datetime = NULL
|
||||
CREATE OR ALTER PROCEDURE dbo.USP_CreateUserVerification @UserAccountID_ UNIQUEIDENTIFIER,
|
||||
@VerificationDateTime DATETIME = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
@@ -11,10 +10,13 @@ BEGIN
|
||||
|
||||
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
|
||||
(UserAccountId, VerificationDateTime)
|
||||
VALUES
|
||||
(@UserAccountID, @VerificationDateTime);
|
||||
(UserAccountId, VerificationDateTime)
|
||||
VALUES (@UserAccountID_, @VerificationDateTime);
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
END
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
CREATE OR ALTER PROCEDURE dbo.USP_CreateCity
|
||||
(
|
||||
CREATE OR ALTER PROCEDURE dbo.USP_CreateCity(
|
||||
@CityName NVARCHAR(100),
|
||||
@StateProvinceCode NVARCHAR(6)
|
||||
)
|
||||
@@ -8,23 +7,24 @@ BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
DECLARE @StateProvinceId UNIQUEIDENTIFIER = dbo.UDF_GetStateProvinceIdByCode(@StateProvinceCode);
|
||||
IF @StateProvinceId IS NULL
|
||||
BEGIN
|
||||
RAISERROR('State/province not found for code.', 16, 1);
|
||||
RETURN;
|
||||
END
|
||||
BEGIN TRANSACTION
|
||||
DECLARE @StateProvinceId UNIQUEIDENTIFIER = dbo.UDF_GetStateProvinceIdByCode(@StateProvinceCode);
|
||||
IF @StateProvinceId IS NULL
|
||||
BEGIN
|
||||
THROW 50001, 'State/province does not exist', 1;
|
||||
END
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM dbo.City
|
||||
WHERE CityName = @CityName
|
||||
AND StateProvinceID = @StateProvinceId
|
||||
)
|
||||
RETURN;
|
||||
IF EXISTS (SELECT 1
|
||||
FROM dbo.City
|
||||
WHERE CityName = @CityName
|
||||
AND StateProvinceID = @StateProvinceId)
|
||||
BEGIN
|
||||
|
||||
INSERT INTO dbo.City
|
||||
(StateProvinceID, CityName)
|
||||
VALUES
|
||||
(@StateProvinceId, @CityName);
|
||||
THROW 50002, 'City already exists.', 1;
|
||||
END
|
||||
|
||||
INSERT INTO dbo.City
|
||||
(StateProvinceID, CityName)
|
||||
VALUES (@StateProvinceId, @CityName);
|
||||
COMMIT TRANSACTION
|
||||
END;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
CREATE OR ALTER PROCEDURE dbo.USP_CreateCountry
|
||||
(
|
||||
CREATE OR ALTER PROCEDURE dbo.USP_CreateCountry(
|
||||
@CountryName NVARCHAR(100),
|
||||
@ISO3616_1 NVARCHAR(2)
|
||||
)
|
||||
@@ -7,16 +6,15 @@ AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM dbo.Country
|
||||
WHERE ISO3616_1 = @ISO3616_1
|
||||
)
|
||||
RETURN;
|
||||
IF EXISTS (SELECT 1
|
||||
FROM dbo.Country
|
||||
WHERE ISO3616_1 = @ISO3616_1)
|
||||
THROW 50001, 'Country already exists', 1;
|
||||
|
||||
INSERT INTO dbo.Country
|
||||
(CountryName, ISO3616_1)
|
||||
VALUES
|
||||
(@CountryName, @ISO3616_1);
|
||||
VALUES (@CountryName, @ISO3616_1);
|
||||
COMMIT TRANSACTION;
|
||||
END;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
CREATE OR ALTER PROCEDURE dbo.USP_CreateStateProvince
|
||||
(
|
||||
CREATE OR ALTER PROCEDURE dbo.USP_CreateStateProvince(
|
||||
@StateProvinceName NVARCHAR(100),
|
||||
@ISO3616_2 NVARCHAR(6),
|
||||
@CountryCode NVARCHAR(2)
|
||||
@@ -9,22 +8,19 @@ BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM dbo.StateProvince
|
||||
WHERE ISO3616_2 = @ISO3616_2
|
||||
)
|
||||
IF EXISTS (SELECT 1
|
||||
FROM dbo.StateProvince
|
||||
WHERE ISO3616_2 = @ISO3616_2)
|
||||
RETURN;
|
||||
|
||||
DECLARE @CountryId UNIQUEIDENTIFIER = dbo.UDF_GetCountryIdByCode(@CountryCode);
|
||||
IF @CountryId IS NULL
|
||||
BEGIN
|
||||
RAISERROR('Country not found for code.', 16, 1);
|
||||
RETURN;
|
||||
END
|
||||
BEGIN
|
||||
THROW 50001, 'Country does not exist', 1;
|
||||
|
||||
END
|
||||
|
||||
INSERT INTO dbo.StateProvince
|
||||
(StateProvinceName, ISO3616_2, CountryID)
|
||||
VALUES
|
||||
(@StateProvinceName, @ISO3616_2, @CountryId);
|
||||
VALUES (@StateProvinceName, @ISO3616_2, @CountryId);
|
||||
END;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using System.Data.Common;
|
||||
using DataAccessLayer.Sql;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace DataAccessLayer.Repositories
|
||||
{
|
||||
public abstract class Repository<T>(ISqlConnectionFactory connectionFactory)
|
||||
where T : class
|
||||
{
|
||||
protected async Task<SqlConnection> CreateConnection()
|
||||
protected async Task<DbConnection> CreateConnection()
|
||||
{
|
||||
var connection = connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync();
|
||||
@@ -19,6 +19,6 @@ namespace DataAccessLayer.Repositories
|
||||
public abstract Task UpdateAsync(T entity);
|
||||
public abstract Task DeleteAsync(Guid id);
|
||||
|
||||
protected abstract T MapToEntity(SqlDataReader reader);
|
||||
protected abstract T MapToEntity(DbDataReader reader);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using DataAccessLayer.Sql;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace DataAccessLayer.Repositories.UserAccount
|
||||
{
|
||||
public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
|
||||
: 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)
|
||||
{
|
||||
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.Parameters.Add("@UserAccountId", SqlDbType.UniqueIdentifier).Value = userAccount.UserAccountId;
|
||||
command.Parameters.Add("@Username", SqlDbType.NVarChar, 100).Value = userAccount.Username;
|
||||
command.Parameters.Add("@FirstName", SqlDbType.NVarChar, 100).Value = userAccount.FirstName;
|
||||
command.Parameters.Add("@LastName", SqlDbType.NVarChar, 100).Value = userAccount.LastName;
|
||||
command.Parameters.Add("@Email", SqlDbType.NVarChar, 256).Value = userAccount.Email;
|
||||
command.Parameters.Add("@DateOfBirth", SqlDbType.Date).Value = userAccount.DateOfBirth;
|
||||
AddParameter(command, "@UserAccountId", userAccount.UserAccountId);
|
||||
AddParameter(command, "@Username", userAccount.Username);
|
||||
AddParameter(command, "@FirstName", userAccount.FirstName);
|
||||
AddParameter(command, "@LastName", userAccount.LastName);
|
||||
AddParameter(command, "@Email", userAccount.Email);
|
||||
AddParameter(command, "@DateOfBirth", userAccount.DateOfBirth);
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
@@ -26,12 +30,11 @@ namespace DataAccessLayer.Repositories.UserAccount
|
||||
public override async Task<Entities.UserAccount?> GetByIdAsync(Guid id)
|
||||
{
|
||||
await using var connection = await CreateConnection();
|
||||
await using var command = new SqlCommand("usp_GetUserAccountById", connection)
|
||||
{
|
||||
CommandType = CommandType.StoredProcedure
|
||||
};
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = "usp_GetUserAccountById";
|
||||
command.CommandType = CommandType.StoredProcedure;
|
||||
|
||||
command.Parameters.Add("@UserAccountId", SqlDbType.UniqueIdentifier).Value = id;
|
||||
AddParameter(command, "@UserAccountId", id);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
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)
|
||||
{
|
||||
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;
|
||||
|
||||
if (limit.HasValue)
|
||||
command.Parameters.Add("@Limit", SqlDbType.Int).Value = limit.Value;
|
||||
AddParameter(command, "@Limit", limit.Value);
|
||||
|
||||
if (offset.HasValue)
|
||||
command.Parameters.Add("@Offset", SqlDbType.Int).Value = offset.Value;
|
||||
AddParameter(command, "@Offset", offset.Value);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
var users = new List<Entities.UserAccount>();
|
||||
@@ -63,15 +67,16 @@ namespace DataAccessLayer.Repositories.UserAccount
|
||||
public override async Task UpdateAsync(Entities.UserAccount userAccount)
|
||||
{
|
||||
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.Parameters.Add("@UserAccountId", SqlDbType.UniqueIdentifier).Value = userAccount.UserAccountId;
|
||||
command.Parameters.Add("@Username", SqlDbType.NVarChar, 100).Value = userAccount.Username;
|
||||
command.Parameters.Add("@FirstName", SqlDbType.NVarChar, 100).Value = userAccount.FirstName;
|
||||
command.Parameters.Add("@LastName", SqlDbType.NVarChar, 100).Value = userAccount.LastName;
|
||||
command.Parameters.Add("@Email", SqlDbType.NVarChar, 256).Value = userAccount.Email;
|
||||
command.Parameters.Add("@DateOfBirth", SqlDbType.Date).Value = userAccount.DateOfBirth;
|
||||
AddParameter(command, "@UserAccountId", userAccount.UserAccountId);
|
||||
AddParameter(command, "@Username", userAccount.Username);
|
||||
AddParameter(command, "@FirstName", userAccount.FirstName);
|
||||
AddParameter(command, "@LastName", userAccount.LastName);
|
||||
AddParameter(command, "@Email", userAccount.Email);
|
||||
AddParameter(command, "@DateOfBirth", userAccount.DateOfBirth);
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
@@ -79,20 +84,22 @@ namespace DataAccessLayer.Repositories.UserAccount
|
||||
public override async Task DeleteAsync(Guid id)
|
||||
{
|
||||
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.Parameters.Add("@UserAccountId", SqlDbType.UniqueIdentifier).Value = id;
|
||||
AddParameter(command, "@UserAccountId", id);
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
public async Task<Entities.UserAccount?> GetByUsernameAsync(string username)
|
||||
{
|
||||
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.Parameters.Add("@Username", SqlDbType.NVarChar, 100).Value = username;
|
||||
AddParameter(command, "@Username", username);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
return await reader.ReadAsync() ? MapToEntity(reader) : null;
|
||||
@@ -101,16 +108,17 @@ namespace DataAccessLayer.Repositories.UserAccount
|
||||
public async Task<Entities.UserAccount?> GetByEmailAsync(string email)
|
||||
{
|
||||
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.Parameters.Add("@Email", SqlDbType.NVarChar, 256).Value = email;
|
||||
AddParameter(command, "@Email", email);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
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
|
||||
{
|
||||
@@ -129,5 +137,13 @@ namespace DataAccessLayer.Repositories.UserAccount
|
||||
: (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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ public interface IUserCredentialRepository
|
||||
{
|
||||
Task RotateCredentialAsync(Guid userAccountId, UserCredential credential);
|
||||
Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(Guid userAccountId);
|
||||
Task InvalidateCredentialsByUserAccountIdAsync(Guid userAccountId);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Data.Common;
|
||||
using Microsoft.Data.SqlClient;
|
||||
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."
|
||||
);
|
||||
|
||||
public SqlConnection CreateConnection()
|
||||
public DbConnection CreateConnection()
|
||||
{
|
||||
return new SqlConnection(_connectionString);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Microsoft.Data.SqlClient;
|
||||
using System.Data.Common;
|
||||
|
||||
namespace DataAccessLayer.Sql
|
||||
{
|
||||
public interface ISqlConnectionFactory
|
||||
{
|
||||
SqlConnection CreateConnection();
|
||||
DbConnection CreateConnection();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -4,14 +4,21 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>DALTests</RootNamespace>
|
||||
<RootNamespace>Repository.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
<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.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>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user