Implemented HashService and tests
This commit is contained in:
@@ -3,7 +3,7 @@ using LiteCharms.Features.MidrandBooks.Products;
|
|||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
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
|
ILogger<CategorySeederService> logger) : BackgroundService
|
||||||
{
|
{
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
namespace LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
||||||
|
|
||||||
public class CdnSettings
|
public sealed class CdnSettings
|
||||||
{
|
{
|
||||||
public string? BaseCdn { get; set; }
|
public string? BaseCdn { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Orders.Models;
|
|||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
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
|
ILogger<CustomerSeederService> logger) : BackgroundService
|
||||||
{
|
{
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
|||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
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
|
IFeatureManager features, IOptions<CdnSettings> options, ILogger<ProductsSeederService> logger) : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly CdnSettings cdnSettings = options.Value;
|
private readonly CdnSettings cdnSettings = options.Value;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.Models;
|
|||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Tests;
|
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>();
|
private readonly AuthorService authorService = fixture.Services.GetRequiredService<AuthorService>();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using LiteCharms.Features.Extensions;
|
using LiteCharms.Features.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Tests.Common;
|
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.Models;
|
||||||
using LiteCharms.Features.TechShop.Extensions;
|
using LiteCharms.Features.TechShop.Extensions;
|
||||||
using LiteCharms.Features.TechShop.Leads.Models;
|
using LiteCharms.Features.TechShop.Leads.Models;
|
||||||
using LiteCharms.Features.TechShop.Postgres;
|
using LiteCharms.Features.TechShop.Postgres;
|
||||||
using static LiteCharms.Features.Extensions.Hash;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.TechShop.Leads;
|
namespace LiteCharms.Features.TechShop.Leads;
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@ public class LeadService(IDbContextFactory<ShopDbContext> contextFactory)
|
|||||||
FeedItemId = request.FeedItemId,
|
FeedItemId = request.FeedItemId,
|
||||||
Status = LeadStatus.New,
|
Status = LeadStatus.New,
|
||||||
TargetId = request.TargetId,
|
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
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public class Fixture : IDisposable
|
|||||||
.AddGarageS3(Configuration)
|
.AddGarageS3(Configuration)
|
||||||
.AddEmailServices(Configuration)
|
.AddEmailServices(Configuration)
|
||||||
.AddSingleton(Configuration)
|
.AddSingleton(Configuration)
|
||||||
|
.AddHashServices(Configuration)
|
||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
|
|
||||||
Mediator = Services.GetRequiredService<IMediator>();
|
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 -->
|
<!-- Global Usings -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Using Include="System.Net" />
|
||||||
|
<Using Include="System.Text" />
|
||||||
<Using Include="Mediator" />
|
<Using Include="Mediator" />
|
||||||
<Using Include="Xunit.Abstractions" />
|
<Using Include="Xunit.Abstractions" />
|
||||||
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
|
|||||||
@@ -7,19 +7,31 @@ public sealed partial class HashService(IHashids hasher, IOptions<HasherSettings
|
|||||||
{
|
{
|
||||||
private readonly HasherSettings settings = options.Value;
|
private readonly HasherSettings settings = options.Value;
|
||||||
|
|
||||||
[System.Text.RegularExpressions.GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")]
|
[GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")]
|
||||||
private static partial System.Text.RegularExpressions.Regex HexHashRegex();
|
private static partial Regex HexHashRegex { get; }
|
||||||
|
|
||||||
public static readonly Func<string?, string?> StringToSha256Hash = (input) =>
|
[GeneratedRegex(@"\A[0-9a-fA-F]{32}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
|
||||||
|
private static partial Regex Md5Regex { get; }
|
||||||
|
|
||||||
|
[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)));
|
string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input)));
|
||||||
|
|
||||||
public static readonly Func<Stream, string?> StreamToSha256Hash = (stream) =>
|
public static string? StreamToSha256Hash(Stream stream) =>
|
||||||
stream is null ? null : Convert.ToHexString(SHA256.HashData(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));
|
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))
|
if (string.IsNullOrEmpty(input))
|
||||||
return Result.Fail<string>("Input content cannot be null or empty for MD5 processing.");
|
return Result.Fail<string>("Input content cannot be null or empty for MD5 processing.");
|
||||||
@@ -36,16 +48,16 @@ public sealed partial class HashService(IHashids hasher, IOptions<HasherSettings
|
|||||||
return Result.Fail<bool>("Validation failed: Missing signature string parameter.");
|
return Result.Fail<bool>("Validation failed: Missing signature string parameter.");
|
||||||
|
|
||||||
var sortedFields = incomingFormData
|
var sortedFields = incomingFormData
|
||||||
.Where(field => field.Key != "signature")
|
.Where(field => !string.Equals(field.Key, "signature", StringComparison.OrdinalIgnoreCase))
|
||||||
.OrderBy(field => field.Key)
|
.OrderBy(field => field.Key, StringComparer.Ordinal)
|
||||||
.Select(field => $"{field.Key}={Uri.EscapeDataString(field.Value).Replace("%20", "+")}");
|
.Select(field => $"{field.Key}={WebUtility.UrlEncode(field.Value)}");
|
||||||
|
|
||||||
string payload = string.Join("&", sortedFields);
|
string payload = string.Join("&", sortedFields);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase))
|
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)
|
if (!localHashResult.IsSuccess)
|
||||||
return Result.Fail<bool>(localHashResult.Errors);
|
return Result.Fail<bool>(localHashResult.Errors);
|
||||||
@@ -60,7 +72,7 @@ public sealed partial class HashService(IHashids hasher, IOptions<HasherSettings
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex().IsMatch(input)
|
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input)
|
||||||
? Result.Fail<string>("Input must be a valid hexadecimal string.")
|
? Result.Fail<string>("Input must be a valid hexadecimal string.")
|
||||||
: Result.Ok(hasher.EncodeHex(input));
|
: Result.Ok(hasher.EncodeHex(input));
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,8 @@
|
|||||||
<!-- Shared Usings -->
|
<!-- Shared Usings -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="HashidsNet" />
|
<Using Include="HashidsNet" />
|
||||||
|
<Using Include="System.Net" />
|
||||||
|
<Using Include="System.Text.RegularExpressions" />
|
||||||
<Using Include="System.Globalization" />
|
<Using Include="System.Globalization" />
|
||||||
<Using Include="Microsoft.AspNetCore.Builder" />
|
<Using Include="Microsoft.AspNetCore.Builder" />
|
||||||
<Using Include="Microsoft.Extensions.Hosting" />
|
<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