Format infrastructure dir

This commit is contained in:
Aaron Po
2026-02-12 01:10:04 -05:00
parent f48b8452d3
commit 74c5528ea2
16 changed files with 428 additions and 240 deletions

View File

@@ -7,7 +7,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.2.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
<PackageReference
Include="Microsoft.IdentityModel.JsonWebTokens"
Version="8.2.1"
/>
<PackageReference
Include="System.IdentityModel.Tokens.Jwt"
Version="8.2.1"
/>
</ItemGroup>
</Project>

View File

@@ -5,21 +5,27 @@ using Microsoft.IdentityModel.Tokens;
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
namespace Service.Core.Jwt;
public class JwtService : IJwtService
{
private readonly string? _secret = Environment.GetEnvironmentVariable("JWT_SECRET");
private readonly string? _secret = Environment.GetEnvironmentVariable(
"JWT_SECRET"
);
public string GenerateJwt(Guid userId, string username, DateTime expiry)
{
var handler = new JsonWebTokenHandler();
var key = Encoding.UTF8.GetBytes(_secret ?? throw new InvalidOperationException("secret not set"));
var key = Encoding.UTF8.GetBytes(
_secret ?? throw new InvalidOperationException("secret not set")
);
// Base claims (always present)
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, userId.ToString()),
new(JwtRegisteredClaimNames.UniqueName, username),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var tokenDescriptor = new SecurityTokenDescriptor
@@ -28,7 +34,8 @@ public class JwtService : IJwtService
Expires = expiry,
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256)
SecurityAlgorithms.HmacSha256
),
};
return handler.CreateToken(tokenDescriptor);

View File

@@ -7,6 +7,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
<PackageReference
Include="Konscious.Security.Cryptography.Argon2"
Version="1.3.1"
/>
</ItemGroup>
</Project>

View File

@@ -19,7 +19,7 @@ public class PasswordService : IPasswordService
Salt = salt,
DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
MemorySize = ArgonMemoryKb,
Iterations = ArgonIterations
Iterations = ArgonIterations,
};
var hash = argon2.GetBytes(HashSize);
@@ -30,8 +30,12 @@ public class PasswordService : IPasswordService
{
try
{
var parts = stored.Split(':', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2) return false;
var parts = stored.Split(
':',
StringSplitOptions.RemoveEmptyEntries
);
if (parts.Length != 2)
return false;
var salt = Convert.FromBase64String(parts[0]);
var expected = Convert.FromBase64String(parts[1]);
@@ -41,7 +45,7 @@ public class PasswordService : IPasswordService
Salt = salt,
DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
MemorySize = ArgonMemoryKb,
Iterations = ArgonIterations
Iterations = ArgonIterations,
};
var actual = argon2.GetBytes(expected.Length);

View File

@@ -5,13 +5,12 @@ using Repository.Core.Sql;
namespace Repository.Core.Repositories.Auth
{
public class AuthRepository : Repository<Domain.Core.Entities.UserAccount>, IAuthRepository
public class AuthRepository
: Repository<Domain.Core.Entities.UserAccount>,
IAuthRepository
{
public AuthRepository(ISqlConnectionFactory connectionFactory)
: base(connectionFactory)
{
}
: base(connectionFactory) { }
public async Task<Domain.Core.Entities.UserAccount> RegisterUserAsync(
string username,
@@ -19,7 +18,8 @@ namespace Repository.Core.Repositories.Auth
string lastName,
string email,
DateTime dateOfBirth,
string passwordHash)
string passwordHash
)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
@@ -45,12 +45,13 @@ namespace Repository.Core.Repositories.Auth
LastName = lastName,
Email = email,
DateOfBirth = dateOfBirth,
CreatedAt = DateTime.UtcNow
CreatedAt = DateTime.UtcNow,
};
}
public async Task<Domain.Core.Entities.UserAccount?> GetUserByEmailAsync(string email)
public async Task<Domain.Core.Entities.UserAccount?> GetUserByEmailAsync(
string email
)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
@@ -63,8 +64,9 @@ namespace Repository.Core.Repositories.Auth
return await reader.ReadAsync() ? MapToEntity(reader) : null;
}
public async Task<Domain.Core.Entities.UserAccount?> GetUserByUsernameAsync(string username)
public async Task<Domain.Core.Entities.UserAccount?> GetUserByUsernameAsync(
string username
)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
@@ -77,7 +79,9 @@ namespace Repository.Core.Repositories.Auth
return await reader.ReadAsync() ? MapToEntity(reader) : null;
}
public async Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(Guid userAccountId)
public async Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(
Guid userAccountId
)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
@@ -87,10 +91,15 @@ namespace Repository.Core.Repositories.Auth
AddParameter(command, "@UserAccountId", userAccountId);
await using var reader = await command.ExecuteReaderAsync();
return await reader.ReadAsync() ? MapToCredentialEntity(reader) : null;
return await reader.ReadAsync()
? MapToCredentialEntity(reader)
: null;
}
public async Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash)
public async Task RotateCredentialAsync(
Guid userAccountId,
string newPasswordHash
)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
@@ -106,11 +115,15 @@ namespace Repository.Core.Repositories.Auth
/// <summary>
/// Maps a data reader row to a UserAccount entity.
/// </summary>
protected override Domain.Core.Entities.UserAccount MapToEntity(DbDataReader reader)
protected override Domain.Core.Entities.UserAccount MapToEntity(
DbDataReader reader
)
{
return new Domain.Core.Entities.UserAccount
{
UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")),
UserAccountId = reader.GetGuid(
reader.GetOrdinal("UserAccountId")
),
Username = reader.GetString(reader.GetOrdinal("Username")),
FirstName = reader.GetString(reader.GetOrdinal("FirstName")),
LastName = reader.GetString(reader.GetOrdinal("LastName")),
@@ -119,10 +132,12 @@ namespace Repository.Core.Repositories.Auth
UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt"))
? null
: reader.GetDateTime(reader.GetOrdinal("UpdatedAt")),
DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")),
DateOfBirth = reader.GetDateTime(
reader.GetOrdinal("DateOfBirth")
),
Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
? null
: (byte[])reader["Timer"]
: (byte[])reader["Timer"],
};
}
@@ -133,22 +148,34 @@ namespace Repository.Core.Repositories.Auth
{
var entity = new UserCredential
{
UserCredentialId = reader.GetGuid(reader.GetOrdinal("UserCredentialId")),
UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")),
UserCredentialId = reader.GetGuid(
reader.GetOrdinal("UserCredentialId")
),
UserAccountId = reader.GetGuid(
reader.GetOrdinal("UserAccountId")
),
Hash = reader.GetString(reader.GetOrdinal("Hash")),
CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt"))
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;
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"];
entity.Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
? null
: (byte[])reader["Timer"];
}
return entity;
@@ -157,7 +184,11 @@ namespace Repository.Core.Repositories.Auth
/// <summary>
/// Helper method to add a parameter to a database command.
/// </summary>
private static void AddParameter(DbCommand command, string name, object? value)
private static void AddParameter(
DbCommand command,
string name,
object? value
)
{
var p = command.CreateParameter();
p.ParameterName = name;

View File

@@ -24,7 +24,8 @@ namespace Repository.Core.Repositories.Auth
string lastName,
string email,
DateTime dateOfBirth,
string passwordHash);
string passwordHash
);
/// <summary>
/// Retrieves a user account by email address (typically used for login).
@@ -32,7 +33,9 @@ namespace Repository.Core.Repositories.Auth
/// </summary>
/// <param name="email">Email address to search for</param>
/// <returns>UserAccount if found, null otherwise</returns>
Task<Domain.Core.Entities.UserAccount?> GetUserByEmailAsync(string email);
Task<Domain.Core.Entities.UserAccount?> GetUserByEmailAsync(
string email
);
/// <summary>
/// Retrieves a user account by username (typically used for login).
@@ -40,7 +43,9 @@ namespace Repository.Core.Repositories.Auth
/// </summary>
/// <param name="username">Username to search for</param>
/// <returns>UserAccount if found, null otherwise</returns>
Task<Domain.Core.Entities.UserAccount?> GetUserByUsernameAsync(string username);
Task<Domain.Core.Entities.UserAccount?> GetUserByUsernameAsync(
string username
);
/// <summary>
/// Retrieves the active (non-revoked) credential for a user account.
@@ -48,7 +53,9 @@ namespace Repository.Core.Repositories.Auth
/// </summary>
/// <param name="userAccountId">ID of the user account</param>
/// <returns>Active UserCredential if found, null otherwise</returns>
Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(Guid userAccountId);
Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(
Guid userAccountId
);
/// <summary>
/// Rotates a user's credential by invalidating all existing credentials and creating a new one.

View File

@@ -1,15 +1,19 @@
using Domain.Core.Entities;
namespace Repository.Core.Repositories.UserAccount
{
public interface IUserAccountRepository
{
Task<Domain.Core.Entities.UserAccount?> GetByIdAsync(Guid id);
Task<IEnumerable<Domain.Core.Entities.UserAccount>> GetAllAsync(int? limit, int? offset);
Task<IEnumerable<Domain.Core.Entities.UserAccount>> GetAllAsync(
int? limit,
int? offset
);
Task UpdateAsync(Domain.Core.Entities.UserAccount userAccount);
Task DeleteAsync(Guid id);
Task<Domain.Core.Entities.UserAccount?> GetByUsernameAsync(string username);
Task<Domain.Core.Entities.UserAccount?> GetByUsernameAsync(
string username
);
Task<Domain.Core.Entities.UserAccount?> GetByEmailAsync(string email);
}
}

View File

@@ -6,9 +6,12 @@ using Repository.Core.Sql;
namespace Repository.Core.Repositories.UserAccount
{
public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
: Repository<Domain.Core.Entities.UserAccount>(connectionFactory), IUserAccountRepository
: Repository<Domain.Core.Entities.UserAccount>(connectionFactory),
IUserAccountRepository
{
public async Task<Domain.Core.Entities.UserAccount?> GetByIdAsync(Guid id)
public async Task<Domain.Core.Entities.UserAccount?> GetByIdAsync(
Guid id
)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
@@ -21,7 +24,9 @@ namespace Repository.Core.Repositories.UserAccount
return await reader.ReadAsync() ? MapToEntity(reader) : null;
}
public async Task<IEnumerable<Domain.Core.Entities.UserAccount>> GetAllAsync(int? limit, int? offset)
public async Task<
IEnumerable<Domain.Core.Entities.UserAccount>
> GetAllAsync(int? limit, int? offset)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
@@ -45,7 +50,9 @@ namespace Repository.Core.Repositories.UserAccount
return users;
}
public async Task UpdateAsync(Domain.Core.Entities.UserAccount userAccount)
public async Task UpdateAsync(
Domain.Core.Entities.UserAccount userAccount
)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
@@ -73,7 +80,9 @@ namespace Repository.Core.Repositories.UserAccount
await command.ExecuteNonQueryAsync();
}
public async Task<Domain.Core.Entities.UserAccount?> GetByUsernameAsync(string username)
public async Task<Domain.Core.Entities.UserAccount?> GetByUsernameAsync(
string username
)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
@@ -86,7 +95,9 @@ namespace Repository.Core.Repositories.UserAccount
return await reader.ReadAsync() ? MapToEntity(reader) : null;
}
public async Task<Domain.Core.Entities.UserAccount?> GetByEmailAsync(string email)
public async Task<Domain.Core.Entities.UserAccount?> GetByEmailAsync(
string email
)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
@@ -99,11 +110,15 @@ namespace Repository.Core.Repositories.UserAccount
return await reader.ReadAsync() ? MapToEntity(reader) : null;
}
protected override Domain.Core.Entities.UserAccount MapToEntity(DbDataReader reader)
protected override Domain.Core.Entities.UserAccount MapToEntity(
DbDataReader reader
)
{
return new Domain.Core.Entities.UserAccount
{
UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")),
UserAccountId = reader.GetGuid(
reader.GetOrdinal("UserAccountId")
),
Username = reader.GetString(reader.GetOrdinal("Username")),
FirstName = reader.GetString(reader.GetOrdinal("FirstName")),
LastName = reader.GetString(reader.GetOrdinal("LastName")),
@@ -112,14 +127,20 @@ namespace Repository.Core.Repositories.UserAccount
UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt"))
? null
: reader.GetDateTime(reader.GetOrdinal("UpdatedAt")),
DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")),
DateOfBirth = reader.GetDateTime(
reader.GetOrdinal("DateOfBirth")
),
Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
? null
: (byte[])reader["Timer"]
: (byte[])reader["Timer"],
};
}
private static void AddParameter(DbCommand command, string name, object? value)
private static void AddParameter(
DbCommand command,
string name,
object? value
)
{
var p = command.CreateParameter();
p.ParameterName = name;

View File

@@ -12,7 +12,10 @@
Version="160.1000.6"
/>
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference
Include="Microsoft.Extensions.Configuration.Abstractions"
Version="8.0.0"
/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Domain\Domain.csproj" />

View File

@@ -2,17 +2,21 @@ using System.Data.Common;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
namespace Repository.Core.Sql
{
public class DefaultSqlConnectionFactory(IConfiguration configuration) : ISqlConnectionFactory
public class DefaultSqlConnectionFactory(IConfiguration configuration)
: ISqlConnectionFactory
{
private readonly string _connectionString = GetConnectionString(configuration);
private readonly string _connectionString = GetConnectionString(
configuration
);
private static string GetConnectionString(IConfiguration configuration)
{
// Check for full connection string first
var fullConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING");
var fullConnectionString = Environment.GetEnvironmentVariable(
"DB_CONNECTION_STRING"
);
if (!string.IsNullOrEmpty(fullConnectionString))
{
return fullConnectionString;

View File

@@ -12,18 +12,30 @@ namespace Repository.Core.Sql
/// <returns>A properly formatted SQL Server connection string.</returns>
public static string BuildConnectionString(string? databaseName = null)
{
var server = Environment.GetEnvironmentVariable("DB_SERVER")
?? throw new InvalidOperationException("DB_SERVER environment variable is not set");
var server =
Environment.GetEnvironmentVariable("DB_SERVER")
?? throw new InvalidOperationException(
"DB_SERVER environment variable is not set"
);
var dbName = databaseName
var dbName =
databaseName
?? Environment.GetEnvironmentVariable("DB_NAME")
?? throw new InvalidOperationException("DB_NAME environment variable is not set");
?? throw new InvalidOperationException(
"DB_NAME environment variable is not set"
);
var user = Environment.GetEnvironmentVariable("DB_USER")
?? throw new InvalidOperationException("DB_USER environment variable is not set");
var user =
Environment.GetEnvironmentVariable("DB_USER")
?? throw new InvalidOperationException(
"DB_USER environment variable is not set"
);
var password = Environment.GetEnvironmentVariable("DB_PASSWORD")
?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set");
var password =
Environment.GetEnvironmentVariable("DB_PASSWORD")
?? throw new InvalidOperationException(
"DB_PASSWORD environment variable is not set"
);
var builder = new SqlConnectionStringBuilder
{
@@ -32,7 +44,7 @@ namespace Repository.Core.Sql
UserID = user,
Password = password,
TrustServerCertificate = true,
Encrypt = true
Encrypt = true,
};
return builder.ConnectionString;

View File

@@ -1,15 +1,15 @@
using Apps72.Dev.Data.DbMocker;
using Repository.Core.Repositories.Auth;
using FluentAssertions;
using Repository.Tests.Database;
using System.Data;
using Apps72.Dev.Data.DbMocker;
using FluentAssertions;
using Repository.Core.Repositories.Auth;
using Repository.Tests.Database;
namespace Repository.Tests.Auth;
public class AuthRepositoryTest
{
private static AuthRepository CreateRepo(MockDbConnection conn)
=> new(new TestConnectionFactory(conn));
private static AuthRepository CreateRepo(MockDbConnection conn) =>
new(new TestConnectionFactory(conn));
[Fact]
public async Task RegisterUserAsync_CreatesUserWithCredential_ReturnsUserAccount()
@@ -17,10 +17,12 @@ public class AuthRepositoryTest
var expectedUserId = Guid.NewGuid();
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "USP_RegisterUser")
.ReturnsTable(MockTable.WithColumns(("UserAccountId", typeof(Guid)))
.AddRow(expectedUserId));
conn.Mocks.When(cmd => cmd.CommandText == "USP_RegisterUser")
.ReturnsTable(
MockTable
.WithColumns(("UserAccountId", typeof(Guid)))
.AddRow(expectedUserId)
);
var repo = CreateRepo(conn);
var result = await repo.RegisterUserAsync(
@@ -47,9 +49,10 @@ public class AuthRepositoryTest
var userId = Guid.NewGuid();
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.ReturnsTable(MockTable.WithColumns(
conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.ReturnsTable(
MockTable
.WithColumns(
("UserAccountId", typeof(Guid)),
("Username", typeof(string)),
("FirstName", typeof(string)),
@@ -59,7 +62,8 @@ public class AuthRepositoryTest
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[]))
).AddRow(
)
.AddRow(
userId,
"emailuser",
"Email",
@@ -69,7 +73,8 @@ public class AuthRepositoryTest
null,
new DateTime(1990, 5, 15),
null
));
)
);
var repo = CreateRepo(conn);
var result = await repo.GetUserByEmailAsync("emailuser@example.com");
@@ -87,8 +92,7 @@ public class AuthRepositoryTest
{
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn);
@@ -103,9 +107,12 @@ public class AuthRepositoryTest
var userId = Guid.NewGuid();
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername")
.ReturnsTable(MockTable.WithColumns(
conn.Mocks.When(cmd =>
cmd.CommandText == "usp_GetUserAccountByUsername"
)
.ReturnsTable(
MockTable
.WithColumns(
("UserAccountId", typeof(Guid)),
("Username", typeof(string)),
("FirstName", typeof(string)),
@@ -115,7 +122,8 @@ public class AuthRepositoryTest
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[]))
).AddRow(
)
.AddRow(
userId,
"usernameuser",
"Username",
@@ -125,7 +133,8 @@ public class AuthRepositoryTest
null,
new DateTime(1985, 8, 20),
null
));
)
);
var repo = CreateRepo(conn);
var result = await repo.GetUserByUsernameAsync("usernameuser");
@@ -141,8 +150,9 @@ public class AuthRepositoryTest
{
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername")
conn.Mocks.When(cmd =>
cmd.CommandText == "usp_GetUserAccountByUsername"
)
.ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn);
@@ -158,21 +168,26 @@ public class AuthRepositoryTest
var credentialId = Guid.NewGuid();
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId")
.ReturnsTable(MockTable.WithColumns(
conn.Mocks.When(cmd =>
cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId"
)
.ReturnsTable(
MockTable
.WithColumns(
("UserCredentialId", typeof(Guid)),
("UserAccountId", typeof(Guid)),
("Hash", typeof(string)),
("CreatedAt", typeof(DateTime)),
("Timer", typeof(byte[]))
).AddRow(
)
.AddRow(
credentialId,
userId,
"hashed_password_value",
DateTime.UtcNow,
null
));
)
);
var repo = CreateRepo(conn);
var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId);
@@ -189,8 +204,9 @@ public class AuthRepositoryTest
var userId = Guid.NewGuid();
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId")
conn.Mocks.When(cmd =>
cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId"
)
.ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn);
@@ -206,14 +222,14 @@ public class AuthRepositoryTest
var newPasswordHash = "new_hashed_password";
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "USP_RotateUserCredential")
conn.Mocks.When(cmd => cmd.CommandText == "USP_RotateUserCredential")
.ReturnsScalar(1);
var repo = CreateRepo(conn);
// Should not throw
var act = async () => await repo.RotateCredentialAsync(userId, newPasswordHash);
var act = async () =>
await repo.RotateCredentialAsync(userId, newPasswordHash);
await act.Should().NotThrowAsync();
}
}

View File

@@ -15,9 +15,18 @@
<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.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>

View File

@@ -1,22 +1,23 @@
using Apps72.Dev.Data.DbMocker;
using Repository.Core.Repositories.UserAccount;
using FluentAssertions;
using Repository.Core.Repositories.UserAccount;
using Repository.Tests.Database;
namespace Repository.Tests.UserAccount;
public class UserAccountRepositoryTest
{
private static UserAccountRepository CreateRepo(MockDbConnection conn)
=> new(new TestConnectionFactory(conn));
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(
conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountById")
.ReturnsTable(
MockTable
.WithColumns(
("UserAccountId", typeof(Guid)),
("Username", typeof(string)),
("FirstName", typeof(string)),
@@ -26,13 +27,24 @@ public class UserAccountRepositoryTest
("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));
)
.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"));
var result = await repo.GetByIdAsync(
Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
);
result.Should().NotBeNull();
result!.Username.Should().Be("yerb");
@@ -43,9 +55,10 @@ public class UserAccountRepositoryTest
public async Task GetAllAsync_ReturnsMultipleRows()
{
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetAllUserAccounts")
.ReturnsTable(MockTable.WithColumns(
conn.Mocks.When(cmd => cmd.CommandText == "usp_GetAllUserAccounts")
.ReturnsTable(
MockTable
.WithColumns(
("UserAccountId", typeof(Guid)),
("Username", typeof(string)),
("FirstName", typeof(string)),
@@ -55,25 +68,50 @@ public class UserAccountRepositoryTest
("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));
)
.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" });
results
.Select(r => r.Username)
.Should()
.BeEquivalentTo(new[] { "a", "b" });
}
[Fact]
public async Task GetByUsername_ReturnsRow()
{
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername")
.ReturnsTable(MockTable.WithColumns(
conn.Mocks.When(cmd =>
cmd.CommandText == "usp_GetUserAccountByUsername"
)
.ReturnsTable(
MockTable
.WithColumns(
("UserAccountId", typeof(Guid)),
("Username", typeof(string)),
("FirstName", typeof(string)),
@@ -83,8 +121,19 @@ public class UserAccountRepositoryTest
("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));
)
.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");
@@ -96,9 +145,10 @@ public class UserAccountRepositoryTest
public async Task GetByEmail_ReturnsRow()
{
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.ReturnsTable(MockTable.WithColumns(
conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.ReturnsTable(
MockTable
.WithColumns(
("UserAccountId", typeof(Guid)),
("Username", typeof(string)),
("FirstName", typeof(string)),
@@ -108,8 +158,19 @@ public class UserAccountRepositoryTest
("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));
)
.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");