Implemented HashService and tests
This commit is contained in:
@@ -3,7 +3,7 @@ using LiteCharms.Features.MidrandBooks.Products;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
||||
|
||||
public class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features,
|
||||
public sealed class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features,
|
||||
ILogger<CategorySeederService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
||||
|
||||
public class CdnSettings
|
||||
public sealed class CdnSettings
|
||||
{
|
||||
public string? BaseCdn { get; set; }
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Orders.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
||||
|
||||
public class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features,
|
||||
public sealed class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features,
|
||||
ILogger<CustomerSeederService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
|
||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
||||
|
||||
public class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService,
|
||||
public sealed class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService,
|
||||
IFeatureManager features, IOptions<CdnSettings> options, ILogger<ProductsSeederService> logger) : BackgroundService
|
||||
{
|
||||
private readonly CdnSettings cdnSettings = options.Value;
|
||||
|
||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Tests;
|
||||
|
||||
public class AuthorServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
|
||||
public class AuthorServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
|
||||
{
|
||||
private readonly AuthorService authorService = fixture.Services.GetRequiredService<AuthorService>();
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using LiteCharms.Features.Extensions;
|
||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Tests.Common;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using LiteCharms.Features.Extensions;
|
||||
using LiteCharms.Features.Hasher;
|
||||
using LiteCharms.Features.Models;
|
||||
using LiteCharms.Features.TechShop.Extensions;
|
||||
using LiteCharms.Features.TechShop.Leads.Models;
|
||||
using LiteCharms.Features.TechShop.Postgres;
|
||||
using static LiteCharms.Features.Extensions.Hash;
|
||||
|
||||
namespace LiteCharms.Features.TechShop.Leads;
|
||||
|
||||
@@ -29,7 +28,7 @@ public class LeadService(IDbContextFactory<ShopDbContext> contextFactory)
|
||||
FeedItemId = request.FeedItemId,
|
||||
Status = LeadStatus.New,
|
||||
TargetId = request.TargetId,
|
||||
AttributionHash = StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}")
|
||||
AttributionHash = HashService.StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}")
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
|
||||
@@ -26,6 +26,7 @@ public class Fixture : IDisposable
|
||||
.AddGarageS3(Configuration)
|
||||
.AddEmailServices(Configuration)
|
||||
.AddSingleton(Configuration)
|
||||
.AddHashServices(Configuration)
|
||||
.BuildServiceProvider();
|
||||
|
||||
Mediator = Services.GetRequiredService<IMediator>();
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
using LiteCharms.Features.Hasher;
|
||||
|
||||
namespace LiteCharms.Features.Tests;
|
||||
|
||||
public class HashServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
|
||||
{
|
||||
private readonly HashService hashService = fixture.Services.GetRequiredService<HashService>();
|
||||
private readonly string payfastPassphrase = fixture.Configuration.GetSection("HasherSettings:PayfastPassphrase").Value!;
|
||||
|
||||
[Fact]
|
||||
public void StringToSha256Hash_Should_GenerateHash()
|
||||
{
|
||||
var input = "We are the best";
|
||||
var expectedHash = "96E17275B53F6BEB7A0D1C4F789F226D3C71CBE398585F25B3028F2B432E78AB";
|
||||
|
||||
var result = HashService.StringToSha256Hash(input);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(HashService.IsSha256Hash(result));
|
||||
Assert.Equal(expectedHash, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamToSha256Hash_Should_GenerateHash()
|
||||
{
|
||||
var input = "We are successful";
|
||||
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(input));
|
||||
var expectedHash = "C27872EE494B09D72203C98FC858268F3CD3492D62AA7B766A873520C2C73AFB";
|
||||
|
||||
var result = HashService.StreamToSha256Hash(stream);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(HashService.IsSha256Hash(result));
|
||||
Assert.Equal(expectedHash, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BytesToSha256Hash_Should_GenerateHash()
|
||||
{
|
||||
var inputBytes = Encoding.UTF8.GetBytes("We are wealthy");
|
||||
var expectedHash = "3876BF98F6E4A8E42B22C40415687D6FF13F0E887F3F508B71852298FC665737";
|
||||
|
||||
var result = HashService.BytesToSha256Hash(inputBytes);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(HashService.IsSha256Hash(result));
|
||||
Assert.Equal(expectedHash, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToMd5Hash_Should_GenerateHash()
|
||||
{
|
||||
var input = "We manifest our desired destiny";
|
||||
var expectedMd5Lowercase = "6c7816869bcebe4634f7afe9c66dfa08";
|
||||
|
||||
var result = HashService.ToMd5Hash(input);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.True(HashService.IsMd5Hash(result.Value));
|
||||
Assert.Equal(expectedMd5Lowercase, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyPayfastWebhookSignature_Should_GenerateHash()
|
||||
{
|
||||
var paymentId = hashService.HashEncodeLongId(1001).Value;
|
||||
|
||||
var incomingForm = new Dictionary<string, string>
|
||||
{
|
||||
{ "m_payment_id", paymentId },
|
||||
{ "amount", "350.00" },
|
||||
{ "item_name", "System Architecture Book" }
|
||||
};
|
||||
|
||||
var rawPayload = $"amount=350.00&item_name=System+Architecture+Book&m_payment_id={paymentId}&passphrase={payfastPassphrase}";
|
||||
var generatedSignature = HashService.ToMd5Hash(rawPayload).Value;
|
||||
|
||||
var result = hashService.VerifyPayfastWebhookSignature(incomingForm, generatedSignature);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.True(result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HashEncodeHex_Should_GenerateHash()
|
||||
{
|
||||
var validHexInput = "DEADBEEF42";
|
||||
|
||||
var result = hashService.HashEncodeHex(validHexInput);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.False(string.IsNullOrWhiteSpace(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HashEncodeIntId_Should_GenerateHash()
|
||||
{
|
||||
int targetId = 42;
|
||||
|
||||
var result = hashService.HashEncodeIntId(targetId);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.True(result.Value.Length >= 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HashEncodeLongId_Should_GenerateHash()
|
||||
{
|
||||
long targetId = 9904185012L;
|
||||
|
||||
var result = hashService.HashEncodeLongId(targetId);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.True(result.Value.Length >= 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecodeIntIdHash_Should_GenerateHash()
|
||||
{
|
||||
int originalId = 88041;
|
||||
var hashedString = hashService.HashEncodeIntId(originalId).Value;
|
||||
|
||||
var result = hashService.DecodeIntIdHash(hashedString);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(originalId, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecodeLongIdHash_Should_GenerateHash()
|
||||
{
|
||||
long originalId = 9081230491823L;
|
||||
var hashedString = hashService.HashEncodeLongId(originalId).Value;
|
||||
|
||||
var result = hashService.DecodeLongIdHash(hashedString);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(originalId, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecodeHexHash_Should_GenerateHash()
|
||||
{
|
||||
var originalHex = "ABCDEF12345";
|
||||
var hashedString = hashService.HashEncodeHex(originalHex).Value;
|
||||
|
||||
var result = hashService.DecodeHexHash(hashedString);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(originalHex, result.Value);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
|
||||
<!-- Global Usings -->
|
||||
<ItemGroup>
|
||||
<Using Include="System.Net" />
|
||||
<Using Include="System.Text" />
|
||||
<Using Include="Mediator" />
|
||||
<Using Include="Xunit.Abstractions" />
|
||||
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
||||
|
||||
@@ -7,25 +7,37 @@ public sealed partial class HashService(IHashids hasher, IOptions<HasherSettings
|
||||
{
|
||||
private readonly HasherSettings settings = options.Value;
|
||||
|
||||
[System.Text.RegularExpressions.GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")]
|
||||
private static partial System.Text.RegularExpressions.Regex HexHashRegex();
|
||||
[GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")]
|
||||
private static partial Regex HexHashRegex { get; }
|
||||
|
||||
public static readonly Func<string?, string?> StringToSha256Hash = (input) =>
|
||||
string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input)));
|
||||
[GeneratedRegex(@"\A[0-9a-fA-F]{32}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
|
||||
private static partial Regex Md5Regex { get; }
|
||||
|
||||
public static readonly Func<Stream, string?> StreamToSha256Hash = (stream) =>
|
||||
[GeneratedRegex(@"\A[0-9a-fA-F]{64}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
|
||||
private static partial Regex Sha256Regex { get; }
|
||||
|
||||
public static bool IsMd5Hash(string? value) =>
|
||||
!string.IsNullOrWhiteSpace(value) && Md5Regex.IsMatch(value);
|
||||
|
||||
public static bool IsSha256Hash(string? value) =>
|
||||
!string.IsNullOrWhiteSpace(value) && Sha256Regex.IsMatch(value);
|
||||
|
||||
public static string? StringToSha256Hash(string? input) =>
|
||||
string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input)));
|
||||
|
||||
public static string? StreamToSha256Hash(Stream stream) =>
|
||||
stream is null ? null : Convert.ToHexString(SHA256.HashData(stream));
|
||||
|
||||
public static readonly Func<byte[], string?> BytesToSha256Hash = (bytes) =>
|
||||
public static string? BytesToSha256Hash(byte[] bytes) =>
|
||||
bytes is null ? null : Convert.ToHexString(SHA256.HashData(bytes));
|
||||
|
||||
public static Result<string> ComputeMd5Hash(string input)
|
||||
|
||||
public static Result<string> ToMd5Hash(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return Result.Fail<string>("Input content cannot be null or empty for MD5 processing.");
|
||||
|
||||
byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant());
|
||||
return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant());
|
||||
}
|
||||
|
||||
public Result<bool> VerifyPayfastWebhookSignature(IDictionary<string, string> incomingFormData, string incomingSignature)
|
||||
@@ -36,16 +48,16 @@ public sealed partial class HashService(IHashids hasher, IOptions<HasherSettings
|
||||
return Result.Fail<bool>("Validation failed: Missing signature string parameter.");
|
||||
|
||||
var sortedFields = incomingFormData
|
||||
.Where(field => field.Key != "signature")
|
||||
.OrderBy(field => field.Key)
|
||||
.Select(field => $"{field.Key}={Uri.EscapeDataString(field.Value).Replace("%20", "+")}");
|
||||
.Where(field => !string.Equals(field.Key, "signature", StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(field => field.Key, StringComparer.Ordinal)
|
||||
.Select(field => $"{field.Key}={WebUtility.UrlEncode(field.Value)}");
|
||||
|
||||
string payload = string.Join("&", sortedFields);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase))
|
||||
payload += $"&passphrase={Uri.EscapeDataString(settings.PayfastPassphrase).Replace("%20", "+")}";
|
||||
payload += $"&passphrase={WebUtility.UrlEncode(settings.PayfastPassphrase)}";
|
||||
|
||||
var localHashResult = ComputeMd5Hash(payload);
|
||||
var localHashResult = ToMd5Hash(payload);
|
||||
|
||||
if (!localHashResult.IsSuccess)
|
||||
return Result.Fail<bool>(localHashResult.Errors);
|
||||
@@ -60,9 +72,9 @@ public sealed partial class HashService(IHashids hasher, IOptions<HasherSettings
|
||||
}
|
||||
}
|
||||
|
||||
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex().IsMatch(input)
|
||||
? Result.Fail<string>("Input must be a valid hexadecimal string.")
|
||||
: Result.Ok(hasher.EncodeHex(input));
|
||||
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input)
|
||||
? Result.Fail<string>("Input must be a valid hexadecimal string.")
|
||||
: Result.Ok(hasher.EncodeHex(input));
|
||||
|
||||
public Result<string> HashEncodeIntId(int id) => id < 0
|
||||
? Result.Fail<string>("Id cannot be negative.")
|
||||
|
||||
@@ -148,6 +148,8 @@
|
||||
<!-- Shared Usings -->
|
||||
<ItemGroup>
|
||||
<Using Include="HashidsNet" />
|
||||
<Using Include="System.Net" />
|
||||
<Using Include="System.Text.RegularExpressions" />
|
||||
<Using Include="System.Globalization" />
|
||||
<Using Include="Microsoft.AspNetCore.Builder" />
|
||||
<Using Include="Microsoft.Extensions.Hosting" />
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace LiteCharms.Features.Models;
|
||||
|
||||
public class SearchState
|
||||
{
|
||||
public string Query { get; private set; } = string.Empty;
|
||||
|
||||
public event Action? OnSearchSubmitted;
|
||||
|
||||
public void UpdateQuery(string newQuery) => Query = newQuery;
|
||||
|
||||
public void SubmitSearch() => OnSearchSubmitted?.Invoke();
|
||||
}
|
||||
Reference in New Issue
Block a user