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,
|
Timer ROWVERSION,
|
||||||
|
|
||||||
|
|
||||||
CONSTRAINT PK_UserCredential
|
CONSTRAINT PK_UserCredential
|
||||||
PRIMARY KEY (UserCredentialID),
|
PRIMARY KEY (UserCredentialID),
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
CREATE OR ALTER PROCEDURE usp_GetAllUserAccounts
|
CREATE OR ALTER PROCEDURE usp_GetAllUserAccounts
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE usp_GetUserAccountByEmail(
|
||||||
CREATE OR ALTER PROCEDURE usp_GetUserAccountByEmail
|
|
||||||
(
|
|
||||||
@Email VARCHAR(128)
|
@Email VARCHAR(128)
|
||||||
)
|
)
|
||||||
AS
|
AS
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE usp_GetUserAccountByUsername(
|
||||||
CREATE OR ALTER PROCEDURE usp_GetUserAccountByUsername
|
|
||||||
(
|
|
||||||
@Username VARCHAR(64)
|
@Username VARCHAR(64)
|
||||||
)
|
)
|
||||||
AS
|
AS
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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;
|
||||||
@@ -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(
|
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;
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
DECLARE @StateProvinceId UNIQUEIDENTIFIER = dbo.UDF_GetStateProvinceIdByCode(@StateProvinceCode);
|
BEGIN TRANSACTION
|
||||||
IF @StateProvinceId IS NULL
|
DECLARE @StateProvinceId UNIQUEIDENTIFIER = dbo.UDF_GetStateProvinceIdByCode(@StateProvinceCode);
|
||||||
BEGIN
|
IF @StateProvinceId IS NULL
|
||||||
RAISERROR('State/province not found for code.', 16, 1);
|
BEGIN
|
||||||
RETURN;
|
THROW 50001, 'State/province does not exist', 1;
|
||||||
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;
|
|
||||||
|
|
||||||
INSERT INTO dbo.City
|
THROW 50002, 'City already exists.', 1;
|
||||||
(StateProvinceID, CityName)
|
END
|
||||||
VALUES
|
|
||||||
(@StateProvinceId, @CityName);
|
INSERT INTO dbo.City
|
||||||
|
(StateProvinceID, CityName)
|
||||||
|
VALUES (@StateProvinceId, @CityName);
|
||||||
|
COMMIT TRANSACTION
|
||||||
END;
|
END;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
<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>
|
||||||
|
|||||||
@@ -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