Skip to content
This repository has been archived by the owner on Jul 5, 2023. It is now read-only.

Feature/hashedtext type #126

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

namespace Nox.Types.EntityFramework.Types;


public class HashedTextConverter : ValueConverter<HashedText, string>
{
public HashedTextConverter() : base(hashedText => hashedText.Value, hashedTextValue => HashedText.FromHashedValue(hashedTextValue)) { }
}

77 changes: 74 additions & 3 deletions src/Nox.Types/Types/HashedText/HashedText.cs
rochar marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,9 +1,80 @@
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace Nox.Types;

/// <summary>
/// Represents a Nox <see cref="HashedText"/> type and value object.
/// </summary>
public sealed class HashedText : ValueObject<string, HashedText>
{
public HashedText() : base() { Value = string.Empty; }

/// <summary>
/// Represents a Nox <see cref="HashedText"/> type and value object.
/// Creates HashedText object from already hashed value
/// </summary>
/// <remarks>Placeholder, needs to be implemented</remarks>
public sealed class HashedText : ValueObject<uint, HashedText>
/// <param name="hashedValue"></param>
/// <returns>HashedText object</returns>
/// <exception cref="TypeValidationException"></exception>
public static HashedText FromHashedValue(string hashedValue)
{
var newObject = new HashedText
{
Value = hashedValue,
};

var validationResult = newObject.Validate();

if (!validationResult.IsValid)
{
throw new TypeValidationException(validationResult.Errors);
}

return newObject;
}

public static HashedText From(string value, HashedTextTypeOptions options)
{
options ??= new HashedTextTypeOptions();

var newObject = new HashedText
{
Value = HashText(value, options)
};

var validationResult = newObject.Validate();

if (!validationResult.IsValid)
{
throw new TypeValidationException(validationResult.Errors);
}

return newObject;
}

new public static HashedText From(string value)
=> From(value, new HashedTextTypeOptions());

private static string HashText(string plainText, HashedTextTypeOptions hashedTextTypeOptions)
{
string hashedText = string.Empty;
using (var hasher = CreateHasher(hashedTextTypeOptions.HashingAlgorithm))
{
byte[] plainTextBytes = Encoding.UTF8.GetBytes($"{plainText}{hashedTextTypeOptions.Salt}");
byte[] hashBytes = hasher.ComputeHash(plainTextBytes);
hashedText = Convert.ToBase64String(hashBytes);
}

return hashedText;
}


static HashAlgorithm CreateHasher(HashingAlgorithm hashAlgorithm)
{
HashAlgorithm hasher = HashAlgorithm.Create(hashAlgorithm.ToString());

return hasher ?? throw new CryptographicException("Invalid hash algorithm");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Nox.Types;

public class HashedTextTypeOptions
{
public HashingAlgorithm HashingAlgorithm { get; set; } = HashingAlgorithm.SHA256;
public string Salt { get; set; } = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

namespace Nox.Types;

public enum HashingAlgorithm
{
SHA256,
SHA512
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public void Configure(EntityTypeBuilder<Country> builder)
builder.Property(e => e.CountryNumber).HasMaxLength(3).HasConversion<CountryNumberConverter>();
builder.Property(e=>e.MonthOfPeakTourism).HasConversion<MonthToByteConverter>();
builder.Property(e => e.DistanceInKm).HasConversion<DistanceToKilometerConverter>();
builder.Property(e => e.HashedText).HasConversion<HashedTextConverter>();

// Configure Multi-value ValueObjects
builder.OwnsOne(e => e.LatLong).Ignore(p => p.Value);
Expand Down
5 changes: 5 additions & 0 deletions tests/Nox.Types.Tests/EntityFrameworkTests/Models/Country.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,9 @@ public sealed class Country
/// Gets or sets the distance in kilometers.
/// </summary>
public Distance DistanceInKm { get; set; } = null!;

/// <summary>
/// Gets or sets hashed value
/// </summary>
public HashedText HashedText { get; set; } = null!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public void TableShouldGetCreated()
[Fact]
public void AddedItemShouldGetGeneratedId()
{
var newItem = new Country() {
var newItem = new Country() {
Name = Text.From("Switzerland"),
LatLong = LatLong.From(46.802496, 8.234392),
Population = Number.From(8_703_654),
Expand All @@ -28,6 +28,7 @@ public void AddedItemShouldGetGeneratedId()
CountryNumber = CountryNumber.From(756),
MonthOfPeakTourism = Month.From(7),
DistanceInKm = Distance.From(129.522785),
HashedText = HashedText.From("Test123.")
};
DbContext.Countries.Add(newItem);
DbContext.SaveChanges();
Expand All @@ -52,5 +53,6 @@ public void AddedItemShouldGetGeneratedId()
Assert.Equal(7, item.MonthOfPeakTourism.Value);
Assert.Equal(129.522785, item.DistanceInKm.Value);
Assert.Equal(DistanceTypeUnit.Kilometer, item.DistanceInKm.Unit);
Assert.Equal(newItem.HashedText.Value, item.HashedText.Value);
}
}
68 changes: 67 additions & 1 deletion tests/Nox.Types.Tests/Types/HashedText/HashedTextTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,77 @@
// ReSharper disable once CheckNamespace
using System.Security.Cryptography;

namespace Nox.Types.Tests.Types;

public class HashedTextTests
{

rochar marked this conversation as resolved.
Show resolved Hide resolved
[Fact]
public void HashedText_Constructor_WithoutOptions_ReturnsHashedValue()
{
string text = "Text to hash";
var hashedText = HashedText.From(text);

Assert.NotNull(hashedText);
Assert.NotNull(hashedText.Value);
Assert.NotEqual(text, hashedText.Value);
}


[Fact]
public void HashedText_Constructor_WithOptions_ReturnsHashedValue()
{
string text = "Text to hash";
string textHashedExpected = string.Empty;

using (var sha = SHA512.Create())
{
byte[] textData = System.Text.Encoding.UTF8.GetBytes(text);
byte[] hash = sha.ComputeHash(textData);
textHashedExpected = Convert.ToBase64String(hash);
}

var hashedText = HashedText.From(text, new HashedTextTypeOptions() { HashingAlgorithm = HashingAlgorithm.SHA512, Salt="" });

Assert.Equal(textHashedExpected, hashedText.Value);
}

[Fact]
public void HashedText_Equals_ReturnsTrue()
{
string text = "Text to hash";
string textHashedExpected = string.Empty;

using (var sha = SHA512.Create())
{
byte[] textData = System.Text.Encoding.UTF8.GetBytes(text);
byte[] hash = sha.ComputeHash(textData);
textHashedExpected = Convert.ToBase64String(hash);
}

var hashedText = HashedText.From(text, new HashedTextTypeOptions() { HashingAlgorithm = HashingAlgorithm.SHA512, Salt="" });
var expectedHashedText = HashedText.FromHashedValue(textHashedExpected);

Assert.True(hashedText.Equals(expectedHashedText));
}

[Fact]
public void HashedText_Equals_ReturnsFalse_Salting()
{
string text = "Text to hash";
var hashedText = HashedText.From(text, new HashedTextTypeOptions() { HashingAlgorithm = HashingAlgorithm.SHA512, Salt = "salt" });
var hashedTextNoSalting = HashedText.From(text);

Assert.False(hashedText.Equals(hashedTextNoSalting));
}

[Fact]
public void When_Create_Should()
public void HashedText_Equals_ReturnsFalse()
{
string text = "Text to hash";
var hashedText = HashedText.From($"{text} 1");
var expectedHashedText = HashedText.From(text);

Assert.False(hashedText.Equals(expectedHashedText));
}
}