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> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.2.1" /> <PackageReference
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" /> Include="Microsoft.IdentityModel.JsonWebTokens"
Version="8.2.1"
/>
<PackageReference
Include="System.IdentityModel.Tokens.Jwt"
Version="8.2.1"
/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -5,21 +5,27 @@ using Microsoft.IdentityModel.Tokens;
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames; using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
namespace Service.Core.Jwt; namespace Service.Core.Jwt;
public class JwtService : IJwtService 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) public string GenerateJwt(Guid userId, string username, DateTime expiry)
{ {
var handler = new JsonWebTokenHandler(); 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) // Base claims (always present)
var claims = new List<Claim> var claims = new List<Claim>
{ {
new(JwtRegisteredClaimNames.Sub, userId.ToString()), new(JwtRegisteredClaimNames.Sub, userId.ToString()),
new(JwtRegisteredClaimNames.UniqueName, username), new(JwtRegisteredClaimNames.UniqueName, username),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
}; };
var tokenDescriptor = new SecurityTokenDescriptor var tokenDescriptor = new SecurityTokenDescriptor
@@ -28,7 +34,8 @@ public class JwtService : IJwtService
Expires = expiry, Expires = expiry,
SigningCredentials = new SigningCredentials( SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key), new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256) SecurityAlgorithms.HmacSha256
),
}; };
return handler.CreateToken(tokenDescriptor); return handler.CreateToken(tokenDescriptor);

View File

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

View File

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

View File

@@ -5,13 +5,12 @@ using Repository.Core.Sql;
namespace Repository.Core.Repositories.Auth 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) public AuthRepository(ISqlConnectionFactory connectionFactory)
: base(connectionFactory) : base(connectionFactory) { }
{
}
public async Task<Domain.Core.Entities.UserAccount> RegisterUserAsync( public async Task<Domain.Core.Entities.UserAccount> RegisterUserAsync(
string username, string username,
@@ -19,7 +18,8 @@ namespace Repository.Core.Repositories.Auth
string lastName, string lastName,
string email, string email,
DateTime dateOfBirth, DateTime dateOfBirth,
string passwordHash) string passwordHash
)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -45,12 +45,13 @@ namespace Repository.Core.Repositories.Auth
LastName = lastName, LastName = lastName,
Email = email, Email = email,
DateOfBirth = dateOfBirth, DateOfBirth = dateOfBirth,
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow,
}; };
} }
public async Task<Domain.Core.Entities.UserAccount?> GetUserByEmailAsync(
public async Task<Domain.Core.Entities.UserAccount?> GetUserByEmailAsync(string email) string email
)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -63,8 +64,9 @@ namespace Repository.Core.Repositories.Auth
return await reader.ReadAsync() ? MapToEntity(reader) : null; return await reader.ReadAsync() ? MapToEntity(reader) : null;
} }
public async Task<Domain.Core.Entities.UserAccount?> GetUserByUsernameAsync(
public async Task<Domain.Core.Entities.UserAccount?> GetUserByUsernameAsync(string username) string username
)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -77,7 +79,9 @@ namespace Repository.Core.Repositories.Auth
return await reader.ReadAsync() ? MapToEntity(reader) : null; 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 connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -87,10 +91,15 @@ namespace Repository.Core.Repositories.Auth
AddParameter(command, "@UserAccountId", userAccountId); AddParameter(command, "@UserAccountId", userAccountId);
await using var reader = await command.ExecuteReaderAsync(); 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 connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -106,11 +115,15 @@ namespace Repository.Core.Repositories.Auth
/// <summary> /// <summary>
/// Maps a data reader row to a UserAccount entity. /// Maps a data reader row to a UserAccount entity.
/// </summary> /// </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 return new Domain.Core.Entities.UserAccount
{ {
UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")), UserAccountId = reader.GetGuid(
reader.GetOrdinal("UserAccountId")
),
Username = reader.GetString(reader.GetOrdinal("Username")), Username = reader.GetString(reader.GetOrdinal("Username")),
FirstName = reader.GetString(reader.GetOrdinal("FirstName")), FirstName = reader.GetString(reader.GetOrdinal("FirstName")),
LastName = reader.GetString(reader.GetOrdinal("LastName")), LastName = reader.GetString(reader.GetOrdinal("LastName")),
@@ -119,10 +132,12 @@ namespace Repository.Core.Repositories.Auth
UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt")) UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt"))
? null ? null
: reader.GetDateTime(reader.GetOrdinal("UpdatedAt")), : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")),
DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")), DateOfBirth = reader.GetDateTime(
reader.GetOrdinal("DateOfBirth")
),
Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
? null ? null
: (byte[])reader["Timer"] : (byte[])reader["Timer"],
}; };
} }
@@ -133,22 +148,34 @@ namespace Repository.Core.Repositories.Auth
{ {
var entity = new UserCredential var entity = new UserCredential
{ {
UserCredentialId = reader.GetGuid(reader.GetOrdinal("UserCredentialId")), UserCredentialId = reader.GetGuid(
UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")), reader.GetOrdinal("UserCredentialId")
),
UserAccountId = reader.GetGuid(
reader.GetOrdinal("UserAccountId")
),
Hash = reader.GetString(reader.GetOrdinal("Hash")), Hash = reader.GetString(reader.GetOrdinal("Hash")),
CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")) CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")),
}; };
// Optional columns // Optional columns
var hasTimer = reader.GetSchemaTable()?.Rows var hasTimer =
.Cast<System.Data.DataRow>() reader
.Any(r => string.Equals(r["ColumnName"]?.ToString(), "Timer", .GetSchemaTable()
StringComparison.OrdinalIgnoreCase)) ?? ?.Rows.Cast<System.Data.DataRow>()
false; .Any(r =>
string.Equals(
r["ColumnName"]?.ToString(),
"Timer",
StringComparison.OrdinalIgnoreCase
)
) ?? false;
if (hasTimer) 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; return entity;
@@ -157,7 +184,11 @@ namespace Repository.Core.Repositories.Auth
/// <summary> /// <summary>
/// Helper method to add a parameter to a database command. /// Helper method to add a parameter to a database command.
/// </summary> /// </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(); var p = command.CreateParameter();
p.ParameterName = name; p.ParameterName = name;

View File

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

View File

@@ -6,9 +6,12 @@ using Repository.Core.Sql;
namespace Repository.Core.Repositories.UserAccount namespace Repository.Core.Repositories.UserAccount
{ {
public class UserAccountRepository(ISqlConnectionFactory connectionFactory) 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 connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -21,7 +24,9 @@ namespace Repository.Core.Repositories.UserAccount
return await reader.ReadAsync() ? MapToEntity(reader) : null; 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 connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -45,7 +50,9 @@ namespace Repository.Core.Repositories.UserAccount
return users; 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 connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -73,7 +80,9 @@ namespace Repository.Core.Repositories.UserAccount
await command.ExecuteNonQueryAsync(); 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 connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -86,7 +95,9 @@ namespace Repository.Core.Repositories.UserAccount
return await reader.ReadAsync() ? MapToEntity(reader) : null; 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 connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -99,11 +110,15 @@ namespace Repository.Core.Repositories.UserAccount
return await reader.ReadAsync() ? MapToEntity(reader) : null; 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 return new Domain.Core.Entities.UserAccount
{ {
UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")), UserAccountId = reader.GetGuid(
reader.GetOrdinal("UserAccountId")
),
Username = reader.GetString(reader.GetOrdinal("Username")), Username = reader.GetString(reader.GetOrdinal("Username")),
FirstName = reader.GetString(reader.GetOrdinal("FirstName")), FirstName = reader.GetString(reader.GetOrdinal("FirstName")),
LastName = reader.GetString(reader.GetOrdinal("LastName")), LastName = reader.GetString(reader.GetOrdinal("LastName")),
@@ -112,14 +127,20 @@ namespace Repository.Core.Repositories.UserAccount
UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt")) UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt"))
? null ? null
: reader.GetDateTime(reader.GetOrdinal("UpdatedAt")), : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")),
DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")), DateOfBirth = reader.GetDateTime(
reader.GetOrdinal("DateOfBirth")
),
Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
? null ? 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(); var p = command.CreateParameter();
p.ParameterName = name; p.ParameterName = name;

View File

@@ -12,7 +12,10 @@
Version="160.1000.6" Version="160.1000.6"
/> />
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" /> <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>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\Domain\Domain.csproj" /> <ProjectReference Include="..\..\..\Domain\Domain.csproj" />

View File

@@ -2,17 +2,21 @@ using System.Data.Common;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
namespace Repository.Core.Sql 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) private static string GetConnectionString(IConfiguration configuration)
{ {
// Check for full connection string first // Check for full connection string first
var fullConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); var fullConnectionString = Environment.GetEnvironmentVariable(
"DB_CONNECTION_STRING"
);
if (!string.IsNullOrEmpty(fullConnectionString)) if (!string.IsNullOrEmpty(fullConnectionString))
{ {
return fullConnectionString; return fullConnectionString;

View File

@@ -12,18 +12,30 @@ namespace Repository.Core.Sql
/// <returns>A properly formatted SQL Server connection string.</returns> /// <returns>A properly formatted SQL Server connection string.</returns>
public static string BuildConnectionString(string? databaseName = null) public static string BuildConnectionString(string? databaseName = null)
{ {
var server = Environment.GetEnvironmentVariable("DB_SERVER") var server =
?? throw new InvalidOperationException("DB_SERVER environment variable is not set"); Environment.GetEnvironmentVariable("DB_SERVER")
?? throw new InvalidOperationException(
"DB_SERVER environment variable is not set"
);
var dbName = databaseName var dbName =
databaseName
?? Environment.GetEnvironmentVariable("DB_NAME") ?? 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") var user =
?? throw new InvalidOperationException("DB_USER environment variable is not set"); Environment.GetEnvironmentVariable("DB_USER")
?? throw new InvalidOperationException(
"DB_USER environment variable is not set"
);
var password = Environment.GetEnvironmentVariable("DB_PASSWORD") var password =
?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set"); Environment.GetEnvironmentVariable("DB_PASSWORD")
?? throw new InvalidOperationException(
"DB_PASSWORD environment variable is not set"
);
var builder = new SqlConnectionStringBuilder var builder = new SqlConnectionStringBuilder
{ {
@@ -32,7 +44,7 @@ namespace Repository.Core.Sql
UserID = user, UserID = user,
Password = password, Password = password,
TrustServerCertificate = true, TrustServerCertificate = true,
Encrypt = true Encrypt = true,
}; };
return builder.ConnectionString; 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 System.Data;
using Apps72.Dev.Data.DbMocker;
using FluentAssertions;
using Repository.Core.Repositories.Auth;
using Repository.Tests.Database;
namespace Repository.Tests.Auth; namespace Repository.Tests.Auth;
public class AuthRepositoryTest public class AuthRepositoryTest
{ {
private static AuthRepository CreateRepo(MockDbConnection conn) private static AuthRepository CreateRepo(MockDbConnection conn) =>
=> new(new TestConnectionFactory(conn)); new(new TestConnectionFactory(conn));
[Fact] [Fact]
public async Task RegisterUserAsync_CreatesUserWithCredential_ReturnsUserAccount() public async Task RegisterUserAsync_CreatesUserWithCredential_ReturnsUserAccount()
@@ -17,10 +17,12 @@ public class AuthRepositoryTest
var expectedUserId = Guid.NewGuid(); var expectedUserId = Guid.NewGuid();
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd => cmd.CommandText == "USP_RegisterUser")
.When(cmd => cmd.CommandText == "USP_RegisterUser") .ReturnsTable(
.ReturnsTable(MockTable.WithColumns(("UserAccountId", typeof(Guid))) MockTable
.AddRow(expectedUserId)); .WithColumns(("UserAccountId", typeof(Guid)))
.AddRow(expectedUserId)
);
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
var result = await repo.RegisterUserAsync( var result = await repo.RegisterUserAsync(
@@ -47,9 +49,10 @@ public class AuthRepositoryTest
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail") .ReturnsTable(
.ReturnsTable(MockTable.WithColumns( MockTable
.WithColumns(
("UserAccountId", typeof(Guid)), ("UserAccountId", typeof(Guid)),
("Username", typeof(string)), ("Username", typeof(string)),
("FirstName", typeof(string)), ("FirstName", typeof(string)),
@@ -59,7 +62,8 @@ public class AuthRepositoryTest
("UpdatedAt", typeof(DateTime?)), ("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)), ("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[])) ("Timer", typeof(byte[]))
).AddRow( )
.AddRow(
userId, userId,
"emailuser", "emailuser",
"Email", "Email",
@@ -69,7 +73,8 @@ public class AuthRepositoryTest
null, null,
new DateTime(1990, 5, 15), new DateTime(1990, 5, 15),
null null
)); )
);
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
var result = await repo.GetUserByEmailAsync("emailuser@example.com"); var result = await repo.GetUserByEmailAsync("emailuser@example.com");
@@ -87,8 +92,7 @@ public class AuthRepositoryTest
{ {
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.ReturnsTable(MockTable.Empty()); .ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
@@ -103,9 +107,12 @@ public class AuthRepositoryTest
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd =>
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername") cmd.CommandText == "usp_GetUserAccountByUsername"
.ReturnsTable(MockTable.WithColumns( )
.ReturnsTable(
MockTable
.WithColumns(
("UserAccountId", typeof(Guid)), ("UserAccountId", typeof(Guid)),
("Username", typeof(string)), ("Username", typeof(string)),
("FirstName", typeof(string)), ("FirstName", typeof(string)),
@@ -115,7 +122,8 @@ public class AuthRepositoryTest
("UpdatedAt", typeof(DateTime?)), ("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)), ("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[])) ("Timer", typeof(byte[]))
).AddRow( )
.AddRow(
userId, userId,
"usernameuser", "usernameuser",
"Username", "Username",
@@ -125,7 +133,8 @@ public class AuthRepositoryTest
null, null,
new DateTime(1985, 8, 20), new DateTime(1985, 8, 20),
null null
)); )
);
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
var result = await repo.GetUserByUsernameAsync("usernameuser"); var result = await repo.GetUserByUsernameAsync("usernameuser");
@@ -141,8 +150,9 @@ public class AuthRepositoryTest
{ {
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd =>
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername") cmd.CommandText == "usp_GetUserAccountByUsername"
)
.ReturnsTable(MockTable.Empty()); .ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
@@ -158,21 +168,26 @@ public class AuthRepositoryTest
var credentialId = Guid.NewGuid(); var credentialId = Guid.NewGuid();
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd =>
.When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId") cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId"
.ReturnsTable(MockTable.WithColumns( )
.ReturnsTable(
MockTable
.WithColumns(
("UserCredentialId", typeof(Guid)), ("UserCredentialId", typeof(Guid)),
("UserAccountId", typeof(Guid)), ("UserAccountId", typeof(Guid)),
("Hash", typeof(string)), ("Hash", typeof(string)),
("CreatedAt", typeof(DateTime)), ("CreatedAt", typeof(DateTime)),
("Timer", typeof(byte[])) ("Timer", typeof(byte[]))
).AddRow( )
.AddRow(
credentialId, credentialId,
userId, userId,
"hashed_password_value", "hashed_password_value",
DateTime.UtcNow, DateTime.UtcNow,
null null
)); )
);
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId); var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId);
@@ -189,8 +204,9 @@ public class AuthRepositoryTest
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd =>
.When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId") cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId"
)
.ReturnsTable(MockTable.Empty()); .ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
@@ -206,14 +222,14 @@ public class AuthRepositoryTest
var newPasswordHash = "new_hashed_password"; var newPasswordHash = "new_hashed_password";
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd => cmd.CommandText == "USP_RotateUserCredential")
.When(cmd => cmd.CommandText == "USP_RotateUserCredential")
.ReturnsScalar(1); .ReturnsScalar(1);
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
// Should not throw // Should not throw
var act = async () => await repo.RotateCredentialAsync(userId, newPasswordHash); var act = async () =>
await repo.RotateCredentialAsync(userId, newPasswordHash);
await act.Should().NotThrowAsync(); await act.Should().NotThrowAsync();
} }
} }

View File

@@ -15,9 +15,18 @@
<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="FluentAssertions" Version="6.9.0" />
<PackageReference Include="DbMocker" Version="1.26.0" /> <PackageReference Include="DbMocker" Version="1.26.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" /> <PackageReference
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> Include="Microsoft.Extensions.Configuration"
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" /> 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" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
</ItemGroup> </ItemGroup>

View File

@@ -1,22 +1,23 @@
using Apps72.Dev.Data.DbMocker; using Apps72.Dev.Data.DbMocker;
using Repository.Core.Repositories.UserAccount;
using FluentAssertions; using FluentAssertions;
using Repository.Core.Repositories.UserAccount;
using Repository.Tests.Database; using Repository.Tests.Database;
namespace Repository.Tests.UserAccount; namespace Repository.Tests.UserAccount;
public class UserAccountRepositoryTest public class UserAccountRepositoryTest
{ {
private static UserAccountRepository CreateRepo(MockDbConnection conn) private static UserAccountRepository CreateRepo(MockDbConnection conn) =>
=> new(new TestConnectionFactory(conn)); new(new TestConnectionFactory(conn));
[Fact] [Fact]
public async Task GetByIdAsync_ReturnsRow_Mapped() public async Task GetByIdAsync_ReturnsRow_Mapped()
{ {
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountById")
.When(cmd => cmd.CommandText == "usp_GetUserAccountById") .ReturnsTable(
.ReturnsTable(MockTable.WithColumns( MockTable
.WithColumns(
("UserAccountId", typeof(Guid)), ("UserAccountId", typeof(Guid)),
("Username", typeof(string)), ("Username", typeof(string)),
("FirstName", typeof(string)), ("FirstName", typeof(string)),
@@ -26,13 +27,24 @@ public class UserAccountRepositoryTest
("UpdatedAt", typeof(DateTime?)), ("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)), ("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[])) ("Timer", typeof(byte[]))
).AddRow(Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), )
"yerb", "Aaron", "Po", "aaronpo@example.com", .AddRow(
new DateTime(2020, 1, 1), null, Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
new DateTime(1990, 1, 1), null)); "yerb",
"Aaron",
"Po",
"aaronpo@example.com",
new DateTime(2020, 1, 1),
null,
new DateTime(1990, 1, 1),
null
)
);
var repo = CreateRepo(conn); 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.Should().NotBeNull();
result!.Username.Should().Be("yerb"); result!.Username.Should().Be("yerb");
@@ -43,9 +55,10 @@ public class UserAccountRepositoryTest
public async Task GetAllAsync_ReturnsMultipleRows() public async Task GetAllAsync_ReturnsMultipleRows()
{ {
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd => cmd.CommandText == "usp_GetAllUserAccounts")
.When(cmd => cmd.CommandText == "usp_GetAllUserAccounts") .ReturnsTable(
.ReturnsTable(MockTable.WithColumns( MockTable
.WithColumns(
("UserAccountId", typeof(Guid)), ("UserAccountId", typeof(Guid)),
("Username", typeof(string)), ("Username", typeof(string)),
("FirstName", typeof(string)), ("FirstName", typeof(string)),
@@ -55,25 +68,50 @@ public class UserAccountRepositoryTest
("UpdatedAt", typeof(DateTime?)), ("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)), ("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[])) ("Timer", typeof(byte[]))
).AddRow(Guid.NewGuid(), "a", "A", "A", "a@example.com", DateTime.UtcNow, null, DateTime.UtcNow.Date, )
null) .AddRow(
.AddRow(Guid.NewGuid(), "b", "B", "B", "b@example.com", DateTime.UtcNow, null, DateTime.UtcNow.Date, Guid.NewGuid(),
null)); "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 repo = CreateRepo(conn);
var results = (await repo.GetAllAsync(null, null)).ToList(); var results = (await repo.GetAllAsync(null, null)).ToList();
results.Should().HaveCount(2); 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] [Fact]
public async Task GetByUsername_ReturnsRow() public async Task GetByUsername_ReturnsRow()
{ {
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd =>
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername") cmd.CommandText == "usp_GetUserAccountByUsername"
.ReturnsTable(MockTable.WithColumns( )
.ReturnsTable(
MockTable
.WithColumns(
("UserAccountId", typeof(Guid)), ("UserAccountId", typeof(Guid)),
("Username", typeof(string)), ("Username", typeof(string)),
("FirstName", typeof(string)), ("FirstName", typeof(string)),
@@ -83,8 +121,19 @@ public class UserAccountRepositoryTest
("UpdatedAt", typeof(DateTime?)), ("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)), ("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[])) ("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 repo = CreateRepo(conn);
var result = await repo.GetByUsernameAsync("lookupuser"); var result = await repo.GetByUsernameAsync("lookupuser");
@@ -96,9 +145,10 @@ public class UserAccountRepositoryTest
public async Task GetByEmail_ReturnsRow() public async Task GetByEmail_ReturnsRow()
{ {
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail") .ReturnsTable(
.ReturnsTable(MockTable.WithColumns( MockTable
.WithColumns(
("UserAccountId", typeof(Guid)), ("UserAccountId", typeof(Guid)),
("Username", typeof(string)), ("Username", typeof(string)),
("FirstName", typeof(string)), ("FirstName", typeof(string)),
@@ -108,8 +158,19 @@ public class UserAccountRepositoryTest
("UpdatedAt", typeof(DateTime?)), ("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)), ("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[])) ("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 repo = CreateRepo(conn);
var result = await repo.GetByEmailAsync("byemail@example.com"); var result = await repo.GetByEmailAsync("byemail@example.com");